From d53d72a9d83ccbd29cff1fe0fc6b38f2042b0bd3 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 5 Mar 2026 13:05:02 -0800 Subject: [PATCH 1/3] Startup Bug Fixes --- ...ppConfigurationWatchAutoConfiguration.java | 8 +- .../AppConfigurationProperties.java | 7 +- ...nfigurationWatchAutoConfigurationTest.java | 107 ++++++++++++++++++ .../AppConfigurationPropertiesTest.java | 47 ++++++++ 4 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/AppConfigurationWatchAutoConfigurationTest.java diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/AppConfigurationWatchAutoConfiguration.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/AppConfigurationWatchAutoConfiguration.java index 8c3330ad68263..c4340ad2884c4 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/AppConfigurationWatchAutoConfiguration.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/AppConfigurationWatchAutoConfiguration.java @@ -38,8 +38,12 @@ public AppConfigurationWatchAutoConfiguration() { @ConditionalOnMissingBean AppConfigurationRefresh appConfigurationRefresh(AppConfigurationProperties properties, BootstrapContext context) { AppConfigurationReplicaClientFactory clientFactory = context - .get(AppConfigurationReplicaClientFactory.class); - ReplicaLookUp replicaLookUp = context.get(ReplicaLookUp.class); + .getOrElse(AppConfigurationReplicaClientFactory.class, null); + ReplicaLookUp replicaLookUp = context.getOrElse(ReplicaLookUp.class, null); + + if (clientFactory == null || replicaLookUp == null) { + return null; + } return new AppConfigurationPullRefresh(clientFactory, properties.getRefreshInterval(), replicaLookUp, new AppConfigurationRefreshUtil()); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationProperties.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationProperties.java index 37578b33d7f39..0afc8030706b8 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationProperties.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationProperties.java @@ -101,6 +101,9 @@ public void validateAndInit() { Assert.notEmpty(this.stores, "At least one config store has to be configured."); this.stores.forEach(store -> { + if (!store.isEnabled()) { + return; + } Assert.isTrue( StringUtils.hasText(store.getEndpoint()) || StringUtils.hasText(store.getConnectionString()) || store.getEndpoints().size() > 0 || store.getConnectionStrings().size() > 0, @@ -111,7 +114,9 @@ public void validateAndInit() { Map existingEndpoints = new HashMap<>(); for (ConfigStore store : this.stores) { - + if (!store.isEnabled()) { + continue; + } if (store.getEndpoints().size() > 0) { for (String endpoint : store.getEndpoints()) { if (existingEndpoints.containsKey(endpoint)) { diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/AppConfigurationWatchAutoConfigurationTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/AppConfigurationWatchAutoConfigurationTest.java new file mode 100644 index 0000000000000..2905a3907854a --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/AppConfigurationWatchAutoConfigurationTest.java @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.appconfiguration.config; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.bootstrap.BootstrapContext; + +import com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationReplicaClientFactory; +import com.azure.spring.cloud.appconfiguration.config.implementation.autofailover.ReplicaLookUp; +import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationProperties; +import com.azure.spring.cloud.appconfiguration.config.implementation.properties.ConfigStore; + +public class AppConfigurationWatchAutoConfigurationTest { + + private AppConfigurationWatchAutoConfiguration autoConfiguration; + private AppConfigurationProperties properties; + private BootstrapContext bootstrapContext; + private AppConfigurationReplicaClientFactory clientFactory; + private ReplicaLookUp replicaLookUp; + + @BeforeEach + public void setup() { + autoConfiguration = new AppConfigurationWatchAutoConfiguration(); + properties = new AppConfigurationProperties(); + properties.setRefreshInterval(Duration.ofSeconds(30)); + + ConfigStore store = new ConfigStore(); + store.setEndpoint("https://test.azconfig.io"); + List stores = new ArrayList<>(); + stores.add(store); + properties.setStores(stores); + + bootstrapContext = mock(BootstrapContext.class); + clientFactory = mock(AppConfigurationReplicaClientFactory.class); + replicaLookUp = mock(ReplicaLookUp.class); + } + + @Test + public void appConfigurationRefreshBeanIsCreatedWhenDependenciesExist() { + // Arrange + when(bootstrapContext.getOrElse(AppConfigurationReplicaClientFactory.class, null)) + .thenReturn(clientFactory); + when(bootstrapContext.getOrElse(ReplicaLookUp.class, null)) + .thenReturn(replicaLookUp); + + // Act + AppConfigurationRefresh result = autoConfiguration.appConfigurationRefresh(properties, bootstrapContext); + + // Assert + assertNotNull(result, "AppConfigurationRefresh bean should be created when dependencies exist"); + } + + @Test + public void appConfigurationRefreshBeanIsNotCreatedWhenClientFactoryIsMissing() { + // Arrange + when(bootstrapContext.getOrElse(AppConfigurationReplicaClientFactory.class, null)) + .thenReturn(null); + when(bootstrapContext.getOrElse(ReplicaLookUp.class, null)) + .thenReturn(replicaLookUp); + + // Act + AppConfigurationRefresh result = autoConfiguration.appConfigurationRefresh(properties, bootstrapContext); + + // Assert + assertNull(result, "AppConfigurationRefresh bean should not be created when clientFactory is missing"); + } + + @Test + public void appConfigurationRefreshBeanIsNotCreatedWhenReplicaLookUpIsMissing() { + // Arrange + when(bootstrapContext.getOrElse(AppConfigurationReplicaClientFactory.class, null)) + .thenReturn(clientFactory); + when(bootstrapContext.getOrElse(ReplicaLookUp.class, null)) + .thenReturn(null); + + // Act + AppConfigurationRefresh result = autoConfiguration.appConfigurationRefresh(properties, bootstrapContext); + + // Assert + assertNull(result, "AppConfigurationRefresh bean should not be created when replicaLookUp is missing"); + } + + @Test + public void appConfigurationRefreshBeanIsNotCreatedWhenBothDependenciesAreMissing() { + // Arrange + when(bootstrapContext.getOrElse(AppConfigurationReplicaClientFactory.class, null)) + .thenReturn(null); + when(bootstrapContext.getOrElse(ReplicaLookUp.class, null)) + .thenReturn(null); + + // Act + AppConfigurationRefresh result = autoConfiguration.appConfigurationRefresh(properties, bootstrapContext); + + // Assert + assertNull(result, "AppConfigurationRefresh bean should not be created when both dependencies are missing"); + } +} diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationPropertiesTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationPropertiesTest.java index 6641e1ee7108b..326f859245a2d 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationPropertiesTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationPropertiesTest.java @@ -131,4 +131,51 @@ public void multipleEndpointsTest() { IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> properties.validateAndInit()); assertEquals("Duplicate store name exists.", e.getMessage()); } + + @Test + public void disabledStoreIsSkippedDuringValidation() { + AppConfigurationProperties properties = new AppConfigurationProperties(); + List stores = new ArrayList<>(); + + // Create a disabled store with no connection string (would normally fail validation) + ConfigStore disabledStore = new ConfigStore(); + disabledStore.setEnabled(false); + stores.add(disabledStore); + + // Create an enabled store with valid connection string + ConfigStore enabledStore = new ConfigStore(); + enabledStore.setConnectionString(TEST_CONN_STRING); + stores.add(enabledStore); + + properties.setStores(stores); + + // Should not throw exception even though disabled store has no connection string + properties.validateAndInit(); + + assertEquals(2, properties.getStores().size()); + } + + @Test + public void disabledStoreWithDuplicateEndpointIsAllowed() { + AppConfigurationProperties properties = new AppConfigurationProperties(); + List stores = new ArrayList<>(); + + // Create an enabled store with endpoint + ConfigStore enabledStore = new ConfigStore(); + enabledStore.setConnectionString(TEST_CONN_STRING); + stores.add(enabledStore); + + // Create a disabled store with same endpoint (would normally fail duplicate check) + ConfigStore disabledStore = new ConfigStore(); + disabledStore.setEnabled(false); + disabledStore.setConnectionString(TEST_CONN_STRING); + stores.add(disabledStore); + + properties.setStores(stores); + + // Should not throw exception about duplicate endpoint because second store is disabled + properties.validateAndInit(); + + assertEquals(2, properties.getStores().size()); + } } From 46690db60338d037b41a0295d52dcf83a55f5e61 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 5 Mar 2026 13:13:14 -0800 Subject: [PATCH 2/3] Update README.md --- .../README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/README.md b/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/README.md index 22c0b8ce7a8f9..c38355057fd91 100644 --- a/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/README.md +++ b/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/README.md @@ -55,14 +55,14 @@ spring.config.import | The Spring property that triggers the loading of Azure Ap Name | Description | Required | Default ---|---|---|--- spring.cloud.azure.appconfiguration.stores | List of configuration stores from which to load configuration properties | Yes | true -spring.cloud.azure.appconfiguration.enabled | Whether enable spring-cloud-azure-appconfiguration-config or not | No | true +spring.cloud.azure.appconfiguration.enabled | Whether enable spring-cloud-azure-appconfiguration-config or not. Requires `spring.config.import= optional:azureAppConfiguration`. | No | true spring.cloud.azure.appconfiguration.refresh-interval | Amount of time, of type Duration, configurations are stored before a check can occur. | No | null `spring.cloud.azure.appconfiguration.stores` is a list of stores, where each store follows the following format: Name | Description | Required | Default ---|---|---|--- -spring.cloud.azure.appconfiguration.stores[0].enabled | Whether the store will be loaded. | No | true +spring.cloud.azure.appconfiguration.stores[0].enabled | Whether the store will be loaded. Requires either `spring.config.import= optional:azureAppConfiguration` or another config store to be loaded. | No | true spring.cloud.azure.appconfiguration.stores[0].fail-fast | Whether to throw a `RuntimeException` or not when failing to read from App Configuration during application start-up. If an exception does occur during startup when set to false the store is skipped. | No | true spring.cloud.azure.appconfiguration.stores[0].selects[0].key-filter | The key pattern used to indicate which configuration(s) will be loaded. | No | /application/* spring.cloud.azure.appconfiguration.stores[0].selects[0].label-filter | The label used to indicate which configuration(s) will be loaded. | No | `${spring.profiles.active}` or if null `\0` From 6084a029ebc87866cb10baabfda6bc441839758a Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 5 Mar 2026 13:22:59 -0800 Subject: [PATCH 3/3] Update AppConfigurationProperties.java --- .../properties/AppConfigurationProperties.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationProperties.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationProperties.java index 0afc8030706b8..6516c9c08857e 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationProperties.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationProperties.java @@ -100,16 +100,16 @@ public void setRefreshInterval(Duration refreshInterval) { public void validateAndInit() { Assert.notEmpty(this.stores, "At least one config store has to be configured."); - this.stores.forEach(store -> { + for (ConfigStore store : this.stores) { if (!store.isEnabled()) { - return; + continue; } Assert.isTrue( StringUtils.hasText(store.getEndpoint()) || StringUtils.hasText(store.getConnectionString()) || store.getEndpoints().size() > 0 || store.getConnectionStrings().size() > 0, "Either configuration store name or connection string should be configured."); store.validateAndInit(); - }); + } Map existingEndpoints = new HashMap<>();