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 @@ -185,6 +185,7 @@ public ConfigData load(ConfigDataLoaderContext context, AzureAppConfigDataResour
} catch (Exception e) {
// Store the exception to potentially use if all replicas fail
lastException = e; // Log the specific replica failure with context
replicaClientFactory.backoffClient(resource.getEndpoint(), currentClient.getEndpoint());
AppConfigurationReplicaClient nextClient = replicaClientFactory
.getNextActiveClient(resource.getEndpoint(), false);
logReplicaFailure(currentClient, "exception", nextClient != null, e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ public class AzureAppConfigDataResource extends ConfigDataResource {
*
* @param configStore the configuration store settings containing endpoint, selectors, and other options
* @param profiles the Spring Boot profiles for conditional configuration loading
* @param isRefresh whether this resource supports runtime configuration refresh
* @param startup true if this is a startup load operation, false if it is a refresh operation
* @param refreshInterval the interval at which configuration should be refreshed
*/
AzureAppConfigDataResource(boolean appConfigEnabled, ConfigStore configStore, Profiles profiles, boolean isRefresh,
AzureAppConfigDataResource(boolean appConfigEnabled, ConfigStore configStore, Profiles profiles, boolean startup,
Duration refreshInterval) {
this.configStoreEnabled = appConfigEnabled && configStore.isEnabled();
this.endpoint = configStore.getEndpoint();
Expand All @@ -64,7 +64,7 @@ public class AzureAppConfigDataResource extends ConfigDataResource {
this.trimKeyPrefix = configStore.getTrimKeyPrefix();
this.monitoring = configStore.getMonitoring();
this.profiles = profiles;
this.isRefresh = isRefresh;
this.isRefresh = !startup;
this.refreshInterval = refreshInterval;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ void backoffClient(String endpoint) {
int failedAttempt = client.getFailedAttempts();
long backoffTime = BackoffTimeCalculator.calculateBackoff(failedAttempt);
client.updateBackoffEndTime(Instant.now().plusNanos(backoffTime));
activeClients.removeIf(removeClient -> removeClient.getEndpoint().equals(endpoint));
return;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
// Licensed under the MIT License.
package com.azure.spring.cloud.appconfiguration.config.implementation.properties;

import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.EMPTY_LABEL;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
Expand All @@ -12,6 +11,8 @@
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.EMPTY_LABEL;

import jakarta.annotation.PostConstruct;
import jakarta.validation.constraints.NotNull;

Expand Down Expand Up @@ -80,12 +81,18 @@ public AppConfigurationKeyValueSelector setKeyFilter(String keyFilter) {
* latter label has higher priority
*/
public String[] getLabelFilter(List<String> profiles) {
if (labelFilter == null && profiles.size() > 0) {
Collections.reverse(profiles);
return profiles.toArray(new String[profiles.size()]);
} else if (StringUtils.hasText(snapshotName)) {
if (StringUtils.hasText(snapshotName)) {
return new String[0];
} else if (!StringUtils.hasText(labelFilter)) {
}
if (labelFilter == null && !profiles.isEmpty()) {
List<String> mutableProfiles = new ArrayList<>(profiles);
// Defensive copy: profiles may be immutable when provided by certain Spring Boot contexts,
// such as when obtained from Environment.getActiveProfiles(). See
// https://github.com/Azure/azure-sdk-for-java/issues/32708 for details.
Collections.reverse(mutableProfiles);
return mutableProfiles.toArray(new String[mutableProfiles.size()]);
}
if (!StringUtils.hasText(labelFilter)) {
return EMPTY_LABEL_ARRAY;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ void testEnabledStateWithRefreshScenarios(boolean isRefresh, String scenarioDesc

assertTrue(resource.isConfigStoreEnabled(),
"Config store should be enabled in " + scenarioDescription + " when conditions are met");
assertEquals(isRefresh, resource.isRefresh(),
// You pass in startup, but it becomes is refresh
assertEquals(!isRefresh, resource.isRefresh(),
"Should correctly report refresh state for " + scenarioDescription);
}

Expand All @@ -93,14 +94,14 @@ void testAllPropertiesSetCorrectlyRegardlessOfEnabledState() {
true, configStore, mockProfiles, false, TEST_REFRESH_INTERVAL);

assertTrue(enabledResource.isConfigStoreEnabled());
assertAllPropertiesCorrect(enabledResource, trimKeyPrefixes, selects, featureFlagSelects, false);
assertAllPropertiesCorrect(enabledResource, trimKeyPrefixes, selects, featureFlagSelects, true);

configStore.setEnabled(false);
AzureAppConfigDataResource disabledResource = new AzureAppConfigDataResource(
true, configStore, mockProfiles, true, TEST_REFRESH_INTERVAL);

assertFalse(disabledResource.isConfigStoreEnabled());
assertAllPropertiesCorrect(disabledResource, trimKeyPrefixes, selects, featureFlagSelects, true);
assertAllPropertiesCorrect(disabledResource, trimKeyPrefixes, selects, featureFlagSelects, false);
}

private void assertAllPropertiesCorrect(AzureAppConfigDataResource resource,
Expand Down Expand Up @@ -131,7 +132,7 @@ void testNullRefreshIntervalHandling() {
false, configStore, mockProfiles, true, null);
assertFalse(disabledResource.isConfigStoreEnabled());
assertNull(disabledResource.getRefreshInterval());
assertTrue(disabledResource.isRefresh());
assertFalse(disabledResource.isRefresh());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.spring.cloud.appconfiguration.config.implementation.properties;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.EMPTY_LABEL;

public class AppConfigurationKeyValueSelectorTest {

private AppConfigurationKeyValueSelector selector;

@BeforeEach
public void setup() {
selector = new AppConfigurationKeyValueSelector();
}

@Test
public void getKeyFilterDefaultTest() {
// When no key filter is set, should return default
String keyFilter = selector.getKeyFilter();
assertEquals("/application/", keyFilter);
}

@Test
public void getKeyFilterCustomTest() {
// When custom key filter is set
selector.setKeyFilter("/custom/");
String keyFilter = selector.getKeyFilter();
assertEquals("/custom/", keyFilter);
}

@Test
public void getKeyFilterEmptyTest() {
// When empty key filter is set, should return default
selector.setKeyFilter("");
String keyFilter = selector.getKeyFilter();
assertEquals("/application/", keyFilter);
}

@Test
public void getLabelFilterWithProfilesTest() {
// Test with profiles when labelFilter is null
List<String> profiles = new ArrayList<>(Arrays.asList("dev", "prod", "staging"));

String[] result = selector.getLabelFilter(profiles);

// Should be reversed: staging, prod, dev
assertArrayEquals(new String[]{"staging", "prod", "dev"}, result);
}

@Test
public void getLabelFilterWithEmptyProfilesTest() {
// Test with empty profiles when labelFilter is null
List<String> profiles = new ArrayList<>();

String[] result = selector.getLabelFilter(profiles);

// Should return empty label array
assertArrayEquals(new String[]{EMPTY_LABEL}, result);
}

@Test
public void getLabelFilterWithImmutableProfilesTest() {
// Test with immutable profiles list (simulates the UnsupportedOperationException scenario)
List<String> profiles = List.of("dev", "prod"); // Immutable list

String[] result = selector.getLabelFilter(profiles);

// Should handle immutable list and return reversed: prod, dev
assertArrayEquals(new String[]{"prod", "dev"}, result);
}

@Test
public void getLabelFilterWithSnapshotTest() {
// Test when snapshot is set
selector.setSnapshotName("test-snapshot");
List<String> profiles = Arrays.asList("dev", "prod");

String[] result = selector.getLabelFilter(profiles);

// Should return empty array when snapshot is set
assertArrayEquals(new String[0], result);
}

@Test
public void getLabelFilterWithCustomLabelFilterTest() {
// Test with custom label filter
selector.setLabelFilter("dev,prod,staging");
List<String> profiles = Arrays.asList("ignored");

String[] result = selector.getLabelFilter(profiles);

// Should be reversed and trimmed
assertArrayEquals(new String[]{"staging", "prod", "dev"}, result);
}

@Test
public void getLabelFilterWithLabelFilterTrailingCommaTest() {
// Test with trailing comma in label filter
selector.setLabelFilter("dev,prod,");
List<String> profiles = Arrays.asList("ignored");

String[] result = selector.getLabelFilter(profiles);

// Should include empty label at the end after reversing
assertArrayEquals(new String[]{EMPTY_LABEL, "prod", "dev"}, result);
}

@Test
public void getLabelFilterWithSpacesTest() {
// Test label filter with spaces (should be trimmed)
selector.setLabelFilter(" dev , prod , staging ");
List<String> profiles = Arrays.asList("ignored");

String[] result = selector.getLabelFilter(profiles);

// Should be reversed and trimmed
assertArrayEquals(new String[]{"staging", "prod", "dev"}, result);
}

@Test
public void getLabelFilterWithDuplicatesTest() {
// Test label filter with duplicates (should be distinct)
selector.setLabelFilter("dev,prod,dev,staging,prod");
List<String> profiles = Arrays.asList("ignored");

String[] result = selector.getLabelFilter(profiles);

// Should be reversed, trimmed, and distinct
assertArrayEquals(new String[]{"staging", "prod", "dev"}, result);
}

@Test
public void getLabelFilterWithEmptyLabelsTest() {
// Test label filter with empty labels
selector.setLabelFilter("dev,,prod,");
List<String> profiles = Arrays.asList("ignored");

String[] result = selector.getLabelFilter(profiles);

// Should match the expected order and duplicates, including EMPTY_LABEL
assertArrayEquals(new String[]{EMPTY_LABEL, "prod", EMPTY_LABEL, "dev"}, result);
}

@Test
public void validateAndInitValidConfigurationTest() {
// Test valid configuration
selector.setKeyFilter("/valid/");
selector.setLabelFilter("dev,prod");

// Should not throw any exception
selector.validateAndInit();
}

@Test
public void validateAndInitInvalidKeyFilterTest() {
// Test invalid key filter with asterisk
selector.setKeyFilter("/invalid*filter/");

assertThrows(IllegalArgumentException.class, () -> {
selector.validateAndInit();
});
}

@Test
public void validateAndInitInvalidLabelFilterTest() {
// Test invalid label filter with asterisk
selector.setLabelFilter("dev*,prod");

assertThrows(IllegalArgumentException.class, () -> {
selector.validateAndInit();
});
}

@Test
public void validateAndInitSnapshotWithKeyFilterTest() {
// Test snapshot with key filter (should fail)
selector.setKeyFilter("/test/");
selector.setSnapshotName("test-snapshot");

assertThrows(IllegalArgumentException.class, () -> {
selector.validateAndInit();
});
}

@Test
public void validateAndInitSnapshotWithLabelFilterTest() {
// Test snapshot with label filter (should fail)
selector.setLabelFilter("dev");
selector.setSnapshotName("test-snapshot");

assertThrows(IllegalArgumentException.class, () -> {
selector.validateAndInit();
});
}

@Test
public void validateAndInitValidSnapshotTest() {
// Test valid snapshot configuration (no key or label filters)
selector.setSnapshotName("test-snapshot");

// Should not throw any exception
selector.validateAndInit();
}

@Test
public void mapLabelNullTest() {
// Test the private mapLabel method indirectly through getLabelFilter
selector.setLabelFilter("dev,,prod");
List<String> profiles = Arrays.asList("ignored");

String[] result = selector.getLabelFilter(profiles);

// Empty label should be converted to EMPTY_LABEL constant
assertTrue(Arrays.asList(result).contains(EMPTY_LABEL));
}

@Test
public void complexScenarioTest() {
// Test a complex real-world scenario
List<String> profiles = new ArrayList<>(Arrays.asList("test", "dev", "prod"));

// First call with profiles
String[] profileResult = selector.getLabelFilter(profiles);
assertArrayEquals(new String[]{"prod", "dev", "test"}, profileResult);

// Then set a custom label filter
selector.setLabelFilter("custom1, custom2 ,custom3,");
String[] customResult = selector.getLabelFilter(profiles);
assertArrayEquals(new String[]{EMPTY_LABEL, "custom3", "custom2", "custom1"}, customResult);

// Finally set a snapshot (should override everything)
selector.setSnapshotName("final-snapshot");
String[] snapshotResult = selector.getLabelFilter(profiles);
assertArrayEquals(new String[0], snapshotResult);
}

@Test
public void profilesListModificationSafetyTest() {
// Test that the original profiles list is not modified
List<String> originalProfiles = new ArrayList<>(Arrays.asList("a", "b", "c"));
List<String> profilesCopy = new ArrayList<>(originalProfiles);

selector.getLabelFilter(originalProfiles);

// Original list should remain unchanged
assertEquals(profilesCopy, originalProfiles);
}
}
Loading