diff --git a/springdoc-openapi-starter-common/src/test/java/org/springdoc/core/configuration/SpringDocDataRestConfigurationTest.java b/springdoc-openapi-starter-common/src/test/java/org/springdoc/core/configuration/SpringDocDataRestConfigurationTest.java new file mode 100644 index 0000000000..3b252c12e1 --- /dev/null +++ b/springdoc-openapi-starter-common/src/test/java/org/springdoc/core/configuration/SpringDocDataRestConfigurationTest.java @@ -0,0 +1,118 @@ +package org.springdoc.core.configuration; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springdoc.core.configuration.SpringDocDataRestConfiguration.DataRestHateoasPropertiesConfiguration; +import org.springdoc.core.configuration.SpringDocDataRestConfiguration.DataRestNoHateoasPropertiesConfiguration; +import org.springdoc.core.properties.SpringDocConfigProperties; +import org.springdoc.core.providers.DataRestHalProvider; +import org.springdoc.core.providers.ObjectMapperProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.boot.hateoas.autoconfigure.HateoasProperties; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; + +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SpringDocDataRestConfiguration} and the way it handles the + * optional {@link HateoasProperties} class. + *

+ * There are two concerns being verified here: + *

+ * The absence of {@code HateoasProperties} is simulated with a + * {@link FilteredClassLoader}. A runtime property such as + * {@code springdoc.enable-hateoas=false} cannot be used for this, because it only + * toggles behavior at runtime and never removes the class from the classpath, so the + * {@code @ConditionalOnMissingClass} fallback would never be selected. + */ +class SpringDocDataRestConfigurationTest { + /** + * Structural guarantees that keep {@code HateoasProperties} references out of any + * class Spring might load when the type is absent. + */ + @Nested + class Structure { + @Test + void outerConfigClassHasNoBeanMethodReferencingHateoasProperties() { + for (Method method : SpringDocDataRestConfiguration.class.getDeclaredMethods()) { + if (method.isAnnotationPresent(Bean.class)) { + for (Type type : method.getGenericParameterTypes()) { + assertThat(type.getTypeName()) + .as("@Bean method %s() should not reference HateoasProperties directly", method.getName()) + .doesNotContain(HateoasProperties.class.getSimpleName()); + } + } + } + } + + @Test + void hateoasPropertiesConfigIsGuardedByConditionalOnClass() { + ConditionalOnClass onClass = DataRestHateoasPropertiesConfiguration.class.getAnnotation(ConditionalOnClass.class); + assertThat(onClass) + .as("%s should be guarded by @ConditionalOnClass", DataRestHateoasPropertiesConfiguration.class.getSimpleName()) + .isNotNull(); + assertThat(Arrays.asList(onClass.name())).contains(HateoasProperties.class.getName()); + } + + @Test + void noHateoasPropertiesConfigIsGuardedByConditionalOnMissingClass() { + ConditionalOnMissingClass onMissingClass = DataRestNoHateoasPropertiesConfiguration.class.getAnnotation(ConditionalOnMissingClass.class); + assertThat(onMissingClass) + .as("%s should be guarded by @ConditionalOnMissingClass", DataRestNoHateoasPropertiesConfiguration.class.getSimpleName()) + .isNotNull(); + assertThat(Arrays.asList(onMissingClass.value())).contains(HateoasProperties.class.getName()); + } + } + + /** + * Behavioral verification that the correct nested configuration is selected and a + * {@link DataRestHalProvider} is created in both scenarios. + */ + @Nested + class Wiring { + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withBean(ObjectMapperProvider.class, () -> new ObjectMapperProvider(new SpringDocConfigProperties())) + .withUserConfiguration( + DataRestHateoasPropertiesConfiguration.class, + DataRestNoHateoasPropertiesConfiguration.class); + + @Test + void halProviderIsCreatedFromHateoasPropertiesConfigWhenHateoasPropertiesIsPresent() { + contextRunner + .run(context -> { + assertThat(context).hasNotFailed(); + assertThat(context).hasSingleBean(DataRestHalProvider.class); + assertThat(context).hasSingleBean(DataRestHateoasPropertiesConfiguration.class); + assertThat(context).doesNotHaveBean(DataRestNoHateoasPropertiesConfiguration.class); + }); + } + + @Test + void halProviderIsCreatedFromFallbackConfigWhenHateoasPropertiesIsAbsent() { + contextRunner + .withClassLoader(new FilteredClassLoader(HateoasProperties.class)) + .run(context -> { + assertThat(context).hasNotFailed(); + assertThat(context).hasSingleBean(DataRestHalProvider.class); + assertThat(context).hasSingleBean(DataRestNoHateoasPropertiesConfiguration.class); + assertThat(context).doesNotHaveBean(DataRestHateoasPropertiesConfiguration.class); + }); + } + } +} diff --git a/springdoc-openapi-tests/springdoc-openapi-data-rest-tests/src/test/java/test/org/springdoc/api/v31/app39/SpringDocApp39Test.java b/springdoc-openapi-tests/springdoc-openapi-data-rest-tests/src/test/java/test/org/springdoc/api/v31/app39/SpringDocApp39Test.java deleted file mode 100644 index 65d9ac5e3a..0000000000 --- a/springdoc-openapi-tests/springdoc-openapi-data-rest-tests/src/test/java/test/org/springdoc/api/v31/app39/SpringDocApp39Test.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * - * * - * * * - * * * * - * * * * * - * * * * * * Copyright 2019-2026 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. - * * * * * - * * * * - * * * - * * - * - */ - -package test.org.springdoc.api.v31.app39; - -import java.lang.reflect.Method; -import java.lang.reflect.Type; -import java.util.Arrays; - -import org.junit.jupiter.api.Test; -import org.springdoc.core.configuration.SpringDocDataRestConfiguration; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; -import org.springframework.context.annotation.Bean; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Verifies that HateoasProperties-dependent bean methods are isolated in nested - * configuration classes guarded by {@code @ConditionalOnClass} / - * {@code @ConditionalOnMissingClass}, so that when HateoasProperties is absent - * from the classpath, Spring never loads the class that references it. - * - * @author bnasslahsen - */ -class SpringDocApp39Test { - - private static final String HATEOAS_PROPERTIES_CLASS = "org.springframework.boot.hateoas.autoconfigure.HateoasProperties"; - - @Test - void dataRestConfigOuterClassDoesNotReferenceHateoasProperties() { - assertOuterBeanMethodsDoNotReferenceHateoasProperties(SpringDocDataRestConfiguration.class); - } - - @Test - void dataRestConfigHasGuardedNestedClasses() { - assertHasGuardedNestedClasses(SpringDocDataRestConfiguration.class); - } - - /** - * Asserts that no {@code @Bean} method declared directly on the outer - * configuration class has {@code HateoasProperties} in its generic - * parameter types. Before the fix, the outer class had methods like - * {@code halProvider(Optional...)} which caused - * {@code TypeNotPresentException} when HateoasProperties was absent. - */ - private void assertOuterBeanMethodsDoNotReferenceHateoasProperties(Class configClass) { - for (Method method : configClass.getDeclaredMethods()) { - if (method.isAnnotationPresent(Bean.class)) { - for (Type type : method.getGenericParameterTypes()) { - assertThat(type.getTypeName()) - .as("@Bean method %s.%s() should not reference HateoasProperties directly", - configClass.getSimpleName(), method.getName()) - .doesNotContain("HateoasProperties"); - } - } - } - } - - /** - * Asserts that the configuration class has at least one nested class - * guarded by {@code @ConditionalOnClass(name = "...HateoasProperties")} - * and one by {@code @ConditionalOnMissingClass("...HateoasProperties")}. - */ - private void assertHasGuardedNestedClasses(Class configClass) { - boolean hasConditionalOnClass = false; - boolean hasConditionalOnMissingClass = false; - - for (Class inner : configClass.getDeclaredClasses()) { - ConditionalOnClass onClass = inner.getAnnotation(ConditionalOnClass.class); - if (onClass != null && Arrays.asList(onClass.name()).contains(HATEOAS_PROPERTIES_CLASS)) { - hasConditionalOnClass = true; - } - ConditionalOnMissingClass onMissingClass = inner.getAnnotation(ConditionalOnMissingClass.class); - if (onMissingClass != null && Arrays.asList(onMissingClass.value()).contains(HATEOAS_PROPERTIES_CLASS)) { - hasConditionalOnMissingClass = true; - } - } - - assertThat(hasConditionalOnClass) - .as("%s should have a nested class with @ConditionalOnClass for HateoasProperties", - configClass.getSimpleName()) - .isTrue(); - assertThat(hasConditionalOnMissingClass) - .as("%s should have a nested class with @ConditionalOnMissingClass for HateoasProperties", - configClass.getSimpleName()) - .isTrue(); - } - -}