Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,18 +100,23 @@ 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()) {
continue;
}
Comment thread
mrm9084 marked this conversation as resolved.
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<String, Boolean> 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)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ConfigStore> 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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<ConfigStore> 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<ConfigStore> 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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
Loading