11/*
2- * Copyright 2012-2024 the original author or authors.
2+ * Copyright 2012-2025 the original author or authors.
33 *
44 * Licensed under the Apache License, Version 2.0 (the "License");
55 * you may not use this file except in compliance with the License.
1818
1919import java .util .Collections ;
2020import java .util .function .Function ;
21+ import java .util .function .Predicate ;
22+ import java .util .stream .Stream ;
2123
2224import org .junit .jupiter .api .Test ;
2325import org .junit .jupiter .api .extension .ExtendWith ;
26+ import org .junit .jupiter .params .ParameterizedTest ;
27+ import org .junit .jupiter .params .provider .EnumSource ;
2428
2529import org .springframework .boot .autoconfigure .AutoConfigurations ;
30+ import org .springframework .boot .autoconfigure .condition .ConditionEvaluationReport ;
31+ import org .springframework .boot .autoconfigure .condition .ConditionEvaluationReport .ConditionAndOutcome ;
32+ import org .springframework .boot .autoconfigure .condition .ConditionEvaluationReport .ConditionAndOutcomes ;
33+ import org .springframework .boot .autoconfigure .condition .ConditionOutcome ;
2634import org .springframework .boot .autoconfigure .security .SecurityProperties ;
35+ import org .springframework .boot .autoconfigure .security .servlet .UserDetailsServiceAutoConfiguration .MissingAlternativeOrUserPropertiesConfigured ;
2736import org .springframework .boot .context .properties .EnableConfigurationProperties ;
2837import org .springframework .boot .test .context .FilteredClassLoader ;
38+ import org .springframework .boot .test .context .runner .AbstractApplicationContextRunner ;
2939import org .springframework .boot .test .context .runner .ApplicationContextRunner ;
3040import org .springframework .boot .test .context .runner .ReactiveWebApplicationContextRunner ;
3141import org .springframework .boot .test .context .runner .WebApplicationContextRunner ;
3242import org .springframework .boot .test .system .CapturedOutput ;
3343import org .springframework .boot .test .system .OutputCaptureExtension ;
44+ import org .springframework .context .ConfigurableApplicationContext ;
3445import org .springframework .context .annotation .Bean ;
3546import org .springframework .context .annotation .Configuration ;
3647import org .springframework .context .annotation .Import ;
@@ -70,29 +81,30 @@ class UserDetailsServiceAutoConfigurationTests {
7081
7182 @ Test
7283 void shouldSupplyUserDetailsServiceInServletApp () {
73- this .contextRunner .with (AuthenticationExclude . servletApp ())
84+ this .contextRunner .with (AlternativeFormOfAuthentication . nonPresent ())
7485 .run ((context ) -> assertThat (context ).hasSingleBean (UserDetailsService .class ));
7586 }
7687
7788 @ Test
7889 void shouldNotSupplyUserDetailsServiceInReactiveApp () {
7990 new ReactiveWebApplicationContextRunner ().withUserConfiguration (TestSecurityConfiguration .class )
8091 .withConfiguration (AutoConfigurations .of (UserDetailsServiceAutoConfiguration .class ))
81- .with (AuthenticationExclude . reactiveApp ())
92+ .with (AlternativeFormOfAuthentication . nonPresent ())
8293 .run ((context ) -> assertThat (context ).doesNotHaveBean (UserDetailsService .class ));
8394 }
8495
8596 @ Test
8697 void shouldNotSupplyUserDetailsServiceInNonWebApp () {
8798 new ApplicationContextRunner ().withUserConfiguration (TestSecurityConfiguration .class )
8899 .withConfiguration (AutoConfigurations .of (UserDetailsServiceAutoConfiguration .class ))
89- .with (AuthenticationExclude . noWebApp ())
100+ .with (AlternativeFormOfAuthentication . nonPresent ())
90101 .run ((context ) -> assertThat (context ).doesNotHaveBean (UserDetailsService .class ));
91102 }
92103
93104 @ Test
94105 void testDefaultUsernamePassword (CapturedOutput output ) {
95- this .contextRunner .with (AuthenticationExclude .servletApp ()).run ((context ) -> {
106+ this .contextRunner .with (AlternativeFormOfAuthentication .nonPresent ()).run ((context ) -> {
107+ assertThat (outcomeOfMissingAlternativeCondition (context ).isMatch ()).isTrue ();
96108 UserDetailsService manager = context .getBean (UserDetailsService .class );
97109 assertThat (output ).contains ("Using generated security password:" );
98110 assertThat (manager .loadUserByUsername ("user" )).isNotNull ();
@@ -101,60 +113,68 @@ void testDefaultUsernamePassword(CapturedOutput output) {
101113
102114 @ Test
103115 void defaultUserNotCreatedIfAuthenticationManagerBeanPresent (CapturedOutput output ) {
104- this .contextRunner .withUserConfiguration (TestAuthenticationManagerConfiguration .class ).run ((context ) -> {
105- AuthenticationManager manager = context .getBean (AuthenticationManager .class );
106- assertThat (manager )
107- .isEqualTo (context .getBean (TestAuthenticationManagerConfiguration .class ).authenticationManager );
108- assertThat (output ).doesNotContain ("Using generated security password: " );
109- TestingAuthenticationToken token = new TestingAuthenticationToken ("foo" , "bar" );
110- assertThat (manager .authenticate (token )).isNotNull ();
111- });
116+ this .contextRunner .with (AlternativeFormOfAuthentication .nonPresent ())
117+ .withUserConfiguration (TestAuthenticationManagerConfiguration .class )
118+ .run ((context ) -> {
119+ assertThat (outcomeOfMissingAlternativeCondition (context ).isMatch ()).isTrue ();
120+ AuthenticationManager manager = context .getBean (AuthenticationManager .class );
121+ assertThat (manager )
122+ .isEqualTo (context .getBean (TestAuthenticationManagerConfiguration .class ).authenticationManager );
123+ assertThat (output ).doesNotContain ("Using generated security password: " );
124+ TestingAuthenticationToken token = new TestingAuthenticationToken ("foo" , "bar" );
125+ assertThat (manager .authenticate (token )).isNotNull ();
126+ });
112127 }
113128
114129 @ Test
115130 void defaultUserNotCreatedIfAuthenticationManagerResolverBeanPresent (CapturedOutput output ) {
116- this .contextRunner .withUserConfiguration (TestAuthenticationManagerResolverConfiguration .class )
117- .run ((context ) -> assertThat (output ).doesNotContain ("Using generated security password: " ));
131+ this .contextRunner .with (AlternativeFormOfAuthentication .nonPresent ())
132+ .withUserConfiguration (TestAuthenticationManagerResolverConfiguration .class )
133+ .run ((context ) -> {
134+ assertThat (outcomeOfMissingAlternativeCondition (context ).isMatch ()).isTrue ();
135+ assertThat (output ).doesNotContain ("Using generated security password: " );
136+ });
118137 }
119138
120139 @ Test
121140 void defaultUserNotCreatedIfUserDetailsServiceBeanPresent (CapturedOutput output ) {
122- this .contextRunner .withUserConfiguration (TestUserDetailsServiceConfiguration .class ).run ((context ) -> {
123- UserDetailsService userDetailsService = context .getBean (UserDetailsService .class );
124- assertThat (output ).doesNotContain ("Using generated security password: " );
125- assertThat (userDetailsService .loadUserByUsername ("foo" )).isNotNull ();
126- });
141+ this .contextRunner .with (AlternativeFormOfAuthentication .nonPresent ())
142+ .withUserConfiguration (TestUserDetailsServiceConfiguration .class )
143+ .run ((context ) -> {
144+ assertThat (outcomeOfMissingAlternativeCondition (context ).isMatch ()).isTrue ();
145+ UserDetailsService userDetailsService = context .getBean (UserDetailsService .class );
146+ assertThat (output ).doesNotContain ("Using generated security password: " );
147+ assertThat (userDetailsService .loadUserByUsername ("foo" )).isNotNull ();
148+ });
127149 }
128150
129151 @ Test
130152 void defaultUserNotCreatedIfAuthenticationProviderBeanPresent (CapturedOutput output ) {
131- this .contextRunner .withUserConfiguration (TestAuthenticationProviderConfiguration .class ).run ((context ) -> {
132- AuthenticationProvider provider = context .getBean (AuthenticationProvider .class );
133- assertThat (output ).doesNotContain ("Using generated security password: " );
134- TestingAuthenticationToken token = new TestingAuthenticationToken ("foo" , "bar" );
135- assertThat (provider .authenticate (token )).isNotNull ();
136- });
137- }
138-
139- @ Test
140- void defaultUserNotCreatedIfResourceServerWithOpaqueIsUsed () {
141- this .contextRunner .withUserConfiguration (TestConfigWithIntrospectionClient .class ).run ((context ) -> {
142- assertThat (context ).hasSingleBean (OpaqueTokenIntrospector .class );
143- assertThat (context ).doesNotHaveBean (UserDetailsService .class );
144- });
153+ this .contextRunner .with (AlternativeFormOfAuthentication .nonPresent ())
154+ .withUserConfiguration (TestAuthenticationProviderConfiguration .class )
155+ .run ((context ) -> {
156+ assertThat (outcomeOfMissingAlternativeCondition (context ).isMatch ()).isTrue ();
157+ AuthenticationProvider provider = context .getBean (AuthenticationProvider .class );
158+ assertThat (output ).doesNotContain ("Using generated security password: " );
159+ TestingAuthenticationToken token = new TestingAuthenticationToken ("foo" , "bar" );
160+ assertThat (provider .authenticate (token )).isNotNull ();
161+ });
145162 }
146163
147164 @ Test
148- void defaultUserNotCreatedIfResourceServerWithJWTIsUsed () {
149- this .contextRunner .withUserConfiguration (TestConfigWithJwtDecoder .class ).run ((context ) -> {
150- assertThat (context ).hasSingleBean (JwtDecoder .class );
151- assertThat (context ).doesNotHaveBean (UserDetailsService .class );
152- });
165+ void defaultUserNotCreatedIfJwtDecoderBeanPresent () {
166+ this .contextRunner .with (AlternativeFormOfAuthentication .nonPresent ())
167+ .withUserConfiguration (TestConfigWithJwtDecoder .class )
168+ .run ((context ) -> {
169+ assertThat (outcomeOfMissingAlternativeCondition (context ).isMatch ()).isTrue ();
170+ assertThat (context ).hasSingleBean (JwtDecoder .class );
171+ assertThat (context ).doesNotHaveBean (UserDetailsService .class );
172+ });
153173 }
154174
155175 @ Test
156176 void userDetailsServiceWhenPasswordEncoderAbsentAndDefaultPassword () {
157- this .contextRunner .with (AuthenticationExclude . servletApp ())
177+ this .contextRunner .with (AlternativeFormOfAuthentication . nonPresent ())
158178 .withUserConfiguration (TestSecurityConfiguration .class )
159179 .run (((context ) -> {
160180 InMemoryUserDetailsManager userDetailsService = context .getBean (InMemoryUserDetailsManager .class );
@@ -179,49 +199,33 @@ void userDetailsServiceWhenPasswordEncoderBeanPresent() {
179199 testPasswordEncoding (TestConfigWithPasswordEncoder .class , "secret" , "secret" );
180200 }
181201
182- @ Test
183- void userDetailsServiceWhenClientRegistrationRepositoryPresent () {
184- this .contextRunner
185- .withClassLoader (
186- new FilteredClassLoader (OpaqueTokenIntrospector .class , RelyingPartyRegistrationRepository .class ))
187- .run (((context ) -> assertThat (context ).doesNotHaveBean (InMemoryUserDetailsManager .class )));
188- }
189-
190- @ Test
191- void userDetailsServiceWhenOpaqueTokenIntrospectorPresent () {
192- this .contextRunner
193- .withClassLoader (new FilteredClassLoader (ClientRegistrationRepository .class ,
194- RelyingPartyRegistrationRepository .class ))
195- .run (((context ) -> assertThat (context ).doesNotHaveBean (InMemoryUserDetailsManager .class )));
196- }
197-
198- @ Test
199- void userDetailsServiceWhenRelyingPartyRegistrationRepositoryPresent () {
200- this .contextRunner
201- .withClassLoader (new FilteredClassLoader (ClientRegistrationRepository .class , OpaqueTokenIntrospector .class ))
202- .run (((context ) -> assertThat (context ).doesNotHaveBean (InMemoryUserDetailsManager .class )));
202+ @ ParameterizedTest
203+ @ EnumSource
204+ void whenClassOfAlternativeIsPresentUserDetailsServiceBacksOff (AlternativeFormOfAuthentication alternative ) {
205+ this .contextRunner .with (alternative .present ())
206+ .run ((context ) -> assertThat (context ).doesNotHaveBean (InMemoryUserDetailsManager .class ));
203207 }
204208
205- @ Test
206- void userDetailsServiceWhenRelyingPartyRegistrationRepositoryPresentAndUsernameConfigured () {
207- this .contextRunner
208- .withClassLoader (new FilteredClassLoader (ClientRegistrationRepository .class , OpaqueTokenIntrospector .class ))
209+ @ ParameterizedTest
210+ @ EnumSource
211+ void whenAlternativeIsPresentAndUsernameIsConfiguredThenUserDetailsServiceIsAutoConfigured (
212+ AlternativeFormOfAuthentication alternative ) {
213+ this .contextRunner .with (alternative .present ())
209214 .withPropertyValues ("spring.security.user.name=alice" )
210215 .run (((context ) -> assertThat (context ).hasSingleBean (InMemoryUserDetailsManager .class )));
211216 }
212217
213- @ Test
214- void userDetailsServiceWhenRelyingPartyRegistrationRepositoryPresentAndPasswordConfigured () {
215- this .contextRunner
216- .withClassLoader (new FilteredClassLoader (ClientRegistrationRepository .class , OpaqueTokenIntrospector .class ))
218+ @ ParameterizedTest
219+ @ EnumSource
220+ void whenAlternativeIsPresentAndPasswordIsConfiguredThenUserDetailsServiceIsAutoConfigured (
221+ AlternativeFormOfAuthentication alternative ) {
222+ this .contextRunner .with (alternative .present ())
217223 .withPropertyValues ("spring.security.user.password=secret" )
218224 .run (((context ) -> assertThat (context ).hasSingleBean (InMemoryUserDetailsManager .class )));
219225 }
220226
221227 private void testPasswordEncoding (Class <?> configClass , String providedPassword , String expectedPassword ) {
222- this .contextRunner .with (AuthenticationExclude .servletApp ())
223- .withClassLoader (new FilteredClassLoader (ClientRegistrationRepository .class , OpaqueTokenIntrospector .class ,
224- RelyingPartyRegistrationRepository .class ))
228+ this .contextRunner .with (AlternativeFormOfAuthentication .nonPresent ())
225229 .withUserConfiguration (configClass )
226230 .withPropertyValues ("spring.security.user.password=" + providedPassword )
227231 .run (((context ) -> {
@@ -231,24 +235,16 @@ private void testPasswordEncoding(Class<?> configClass, String providedPassword,
231235 }));
232236 }
233237
234- private static final class AuthenticationExclude {
235-
236- private static final FilteredClassLoader filteredClassLoader = new FilteredClassLoader (
237- ClientRegistrationRepository . class , OpaqueTokenIntrospector .class ,
238- RelyingPartyRegistrationRepository . class );
239-
240- static Function < WebApplicationContextRunner , WebApplicationContextRunner > servletApp () {
241- return ( contextRunner ) -> contextRunner . withClassLoader ( filteredClassLoader );
238+ private ConditionOutcome outcomeOfMissingAlternativeCondition ( ConfigurableApplicationContext context ) {
239+ ConditionAndOutcomes conditionAndOutcomes = ConditionEvaluationReport . get ( context . getBeanFactory ())
240+ . getConditionAndOutcomesBySource ()
241+ . get ( UserDetailsServiceAutoConfiguration .class . getName ());
242+ for ( ConditionAndOutcome conditionAndOutcome : conditionAndOutcomes ) {
243+ if ( conditionAndOutcome . getCondition () instanceof MissingAlternativeOrUserPropertiesConfigured ) {
244+ return conditionAndOutcome . getOutcome ();
245+ }
242246 }
243-
244- static Function <ReactiveWebApplicationContextRunner , ReactiveWebApplicationContextRunner > reactiveApp () {
245- return (contextRunner ) -> contextRunner .withClassLoader (filteredClassLoader );
246- }
247-
248- static Function <ApplicationContextRunner , ApplicationContextRunner > noWebApp () {
249- return (contextRunner ) -> contextRunner .withClassLoader (filteredClassLoader );
250- }
251-
247+ return null ;
252248 }
253249
254250 @ Configuration (proxyBeanMethods = false )
@@ -346,4 +342,41 @@ AuthenticationManagerResolver<?> authenticationManagerResolver() {
346342
347343 }
348344
345+ private enum AlternativeFormOfAuthentication {
346+
347+ CLIENT_REGISTRATION_REPOSITORY (ClientRegistrationRepository .class ),
348+
349+ OPAQUE_TOKEN_INTROSPECTOR (OpaqueTokenIntrospector .class ),
350+
351+ RELYING_PARTY_REGISTRATION_REPOSITORY (RelyingPartyRegistrationRepository .class );
352+
353+ private final Class <?> type ;
354+
355+ AlternativeFormOfAuthentication (Class <?> type ) {
356+ this .type = type ;
357+ }
358+
359+ private Class <?> getType () {
360+ return this .type ;
361+ }
362+
363+ @ SuppressWarnings ("unchecked" )
364+ private <T extends AbstractApplicationContextRunner <?, ?, ?>> Function <T , T > present () {
365+ return (contextRunner ) -> (T ) contextRunner
366+ .withClassLoader (new FilteredClassLoader (Stream .of (AlternativeFormOfAuthentication .values ())
367+ .filter (Predicate .not (this ::equals ))
368+ .map (AlternativeFormOfAuthentication ::getType )
369+ .toArray (Class []::new )));
370+ }
371+
372+ @ SuppressWarnings ("unchecked" )
373+ private static <T extends AbstractApplicationContextRunner <?, ?, ?>> Function <T , T > nonPresent () {
374+ return (contextRunner ) -> (T ) contextRunner
375+ .withClassLoader (new FilteredClassLoader (Stream .of (AlternativeFormOfAuthentication .values ())
376+ .map (AlternativeFormOfAuthentication ::getType )
377+ .toArray (Class []::new )));
378+ }
379+
380+ }
381+
349382}
0 commit comments