Skip to content

Commit 332fd64

Browse files
ivywei0125mrm9084
andauthored
[App Configuration] Add test case for feature management lib (Azure#41498)
* add feature management common test cases * add test case for no filters * initialize feature manager and call `featureManager.isEnabled()` to compare the result * update the schema of test case * get sample file and tests file by listFiles api * update the todo comment. Throw exception and use little endian are breaking change, will have both when update to 6.xx version. * use `getContextClassLoader().getResource` to get the resource folder path * address comment: typo wording error fix, add "@SuppressWarnings" * address comment: typo wording error fix * address comment: update member name * add running log * use contains to filter out the "TargetingFilter.sample.json" * add some info log * sort the file list * move to another the package to avoid making feature manager construction as public * address comment: use logging, remove else * address comment: throw exception when empty "feature_management" section. * address comment: rename symbol * Update ValidationsTest.java * fixing linting * address comment: ignore both targeting filter test case --------- Co-authored-by: Matt Metcalf <[email protected]>
1 parent f2075b7 commit 332fd64

16 files changed

+1297
-33
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
package com.azure.spring.cloud.feature.management;
4+
5+
import static org.junit.jupiter.api.Assertions.assertEquals;
6+
import static org.junit.jupiter.api.Assertions.assertNull;
7+
import static org.mockito.Mockito.when;
8+
9+
import java.io.File;
10+
import java.io.IOException;
11+
import java.net.URL;
12+
import java.nio.file.Files;
13+
import java.util.ArrayList;
14+
import java.util.Arrays;
15+
import java.util.Comparator;
16+
import java.util.LinkedHashMap;
17+
import java.util.List;
18+
import java.util.stream.Stream;
19+
20+
import org.junit.jupiter.api.AfterEach;
21+
import org.junit.jupiter.api.BeforeEach;
22+
import org.junit.jupiter.api.extension.ExtendWith;
23+
import org.junit.jupiter.params.ParameterizedTest;
24+
import org.junit.jupiter.params.provider.Arguments;
25+
import org.junit.jupiter.params.provider.MethodSource;
26+
import org.mockito.Mock;
27+
import org.mockito.Mockito;
28+
import org.mockito.MockitoAnnotations;
29+
import org.slf4j.Logger;
30+
import org.slf4j.LoggerFactory;
31+
import org.springframework.context.ApplicationContext;
32+
import org.springframework.test.context.junit.jupiter.SpringExtension;
33+
34+
import com.azure.spring.cloud.feature.management.filters.TargetingFilter;
35+
import com.azure.spring.cloud.feature.management.filters.TargetingFilterTestContextAccessor;
36+
import com.azure.spring.cloud.feature.management.filters.TimeWindowFilter;
37+
import com.azure.spring.cloud.feature.management.implementation.FeatureManagementConfigProperties;
38+
import com.azure.spring.cloud.feature.management.implementation.FeatureManagementProperties;
39+
import com.azure.spring.cloud.feature.management.validationstests.models.ValidationTestCase;
40+
import com.fasterxml.jackson.core.type.TypeReference;
41+
import com.fasterxml.jackson.databind.MapperFeature;
42+
import com.fasterxml.jackson.databind.ObjectMapper;
43+
import com.fasterxml.jackson.databind.json.JsonMapper;
44+
import com.fasterxml.jackson.databind.type.CollectionType;
45+
import com.fasterxml.jackson.databind.type.TypeFactory;
46+
47+
@ExtendWith(SpringExtension.class)
48+
public class ValidationsTest {
49+
@Mock
50+
private ApplicationContext context;
51+
52+
@Mock
53+
private FeatureManagementConfigProperties configProperties;
54+
55+
private static final Logger LOGGER = LoggerFactory.getLogger(ValidationsTest.class);
56+
57+
private static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder()
58+
.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true).build();
59+
60+
private static final String TEST_CASE_FOLDER_PATH = "validations-tests";
61+
62+
private final String inputsUser = "user";
63+
64+
private final String inputsGroups = "groups";
65+
66+
private static final String SAMPLE_FILE_NAME_FILTER = "sample";
67+
68+
private static final String TESTS_FILE_NAME_FILTER = "tests";
69+
70+
@BeforeEach
71+
public void setup() {
72+
MockitoAnnotations.openMocks(this);
73+
when(configProperties.isFailFast()).thenReturn(true);
74+
when(context.getBean(Mockito.contains("TimeWindow"))).thenReturn(new TimeWindowFilter());
75+
}
76+
77+
@AfterEach
78+
public void cleanup() throws Exception {
79+
MockitoAnnotations.openMocks(this).close();
80+
}
81+
82+
private boolean hasException(ValidationTestCase testCase) {
83+
final String exceptionStr = testCase.getIsEnabled().getException();
84+
return exceptionStr != null && !exceptionStr.isEmpty();
85+
}
86+
87+
private boolean hasInput(ValidationTestCase testCase) {
88+
final LinkedHashMap<String, Object> inputsMap = testCase.getInputs();
89+
return inputsMap != null && !inputsMap.isEmpty();
90+
}
91+
92+
private static File[] getFileList(String fileNameFilter) {
93+
final URL folderUrl = Thread.currentThread().getContextClassLoader().getResource(TEST_CASE_FOLDER_PATH);
94+
assert folderUrl != null;
95+
96+
final File folderFile = new File(folderUrl.getFile());
97+
final File[] filteredFiles = folderFile
98+
.listFiles(pathname -> pathname.getName().toLowerCase().contains(fileNameFilter));
99+
assert filteredFiles != null;
100+
101+
Arrays.sort(filteredFiles, Comparator.comparing(File::getName));
102+
return filteredFiles;
103+
}
104+
105+
private List<ValidationTestCase> readTestcasesFromFile(File testFile) throws IOException {
106+
final String jsonString = Files.readString(testFile.toPath());
107+
final CollectionType typeReference = TypeFactory.defaultInstance().constructCollectionType(List.class,
108+
ValidationTestCase.class);
109+
return OBJECT_MAPPER.readValue(jsonString, typeReference);
110+
}
111+
112+
@SuppressWarnings("unchecked")
113+
private static LinkedHashMap<String, Object> readConfigurationFromFile(File sampleFile) throws IOException {
114+
final String jsonString = Files.readString(sampleFile.toPath());
115+
final LinkedHashMap<String, Object> configurations = OBJECT_MAPPER.readValue(jsonString, new TypeReference<>() {
116+
});
117+
final Object featureManagementSection = configurations.get("feature_management");
118+
if (featureManagementSection.getClass().isAssignableFrom(LinkedHashMap.class)) {
119+
return (LinkedHashMap<String, Object>) featureManagementSection;
120+
}
121+
throw new IllegalArgumentException("feature_management part is not a map");
122+
}
123+
124+
static Stream<Arguments> testProvider() throws IOException {
125+
List<Arguments> arguments = new ArrayList<>();
126+
File[] files = getFileList(TESTS_FILE_NAME_FILTER);
127+
128+
final File[] sampleFiles = getFileList(SAMPLE_FILE_NAME_FILTER);
129+
List<FeatureManagementProperties> properties = new ArrayList<>();
130+
for (File sampleFile : sampleFiles) {
131+
final FeatureManagementProperties managementProperties = new FeatureManagementProperties();
132+
managementProperties.putAll(readConfigurationFromFile(sampleFile));
133+
properties.add(managementProperties);
134+
}
135+
136+
for (int i = 0; i < files.length; i++) {
137+
if (files[i].getName().contains(("TargetingFilter"))) {
138+
continue; // TODO(mametcal). Not run the test case until we release the little endian fix
139+
}
140+
arguments.add(Arguments.of(files[i].getName(), files[i], properties.get(i)));
141+
}
142+
143+
return arguments.stream();
144+
}
145+
146+
@ParameterizedTest(name = "{0}")
147+
@MethodSource("testProvider")
148+
void validationTest(String name, File testsFile, FeatureManagementProperties managementProperties)
149+
throws IOException {
150+
LOGGER.debug("Running test case from file: " + name);
151+
final FeatureManager featureManager = new FeatureManager(context, managementProperties, configProperties);
152+
List<ValidationTestCase> testCases = readTestcasesFromFile(testsFile);
153+
for (ValidationTestCase testCase : testCases) {
154+
LOGGER.debug("Test case : " + testCase.getDescription());
155+
if (hasException(testCase)) { // TODO(mametcal). Currently we didn't throw the exception when parameter is
156+
// invalid
157+
assertNull(managementProperties.getOnOff().get(testCase.getFeatureFlagName()));
158+
continue;
159+
}
160+
if (hasInput(testCase)) { // Set inputs
161+
final Object userObj = testCase.getInputs().get(inputsUser);
162+
final Object groupsObj = testCase.getInputs().get(inputsGroups);
163+
final String user = userObj != null ? userObj.toString() : null;
164+
@SuppressWarnings("unchecked")
165+
final List<String> groups = groupsObj != null ? (List<String>) groupsObj : null;
166+
when(context.getBean(Mockito.contains("Targeting")))
167+
.thenReturn(new TargetingFilter(new TargetingFilterTestContextAccessor(user, groups)));
168+
}
169+
170+
final Boolean result = featureManager.isEnabled(testCase.getFeatureFlagName());
171+
assertEquals(result.toString(), testCase.getIsEnabled().getResult());
172+
}
173+
}
174+
}

sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TargetingFilterTest.java

+12-33
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818
import com.azure.spring.cloud.feature.management.implementation.TestConfiguration;
1919
import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext;
2020
import com.azure.spring.cloud.feature.management.models.TargetingException;
21-
import com.azure.spring.cloud.feature.management.targeting.TargetingContext;
22-
import com.azure.spring.cloud.feature.management.targeting.TargetingContextAccessor;
2321
import com.azure.spring.cloud.feature.management.targeting.TargetingEvaluationOptions;
2422

2523
@SpringBootTest(classes = { TestConfiguration.class, SpringBootTest.class })
@@ -48,10 +46,10 @@ public void targetedUser() {
4846
parameters.put(GROUPS, new LinkedHashMap<String, Object>());
4947
parameters.put(DEFAULT_ROLLOUT_PERCENTAGE, 0);
5048
parameters.put("Exclusion", emptyExclusion());
51-
49+
5250
Map<String, Object> excludes = new LinkedHashMap<>();
5351
Map<String, String> excludedGroups = new LinkedHashMap<>();
54-
52+
5553
excludes.put(GROUPS, excludedGroups);
5654

5755
context.setParameters(parameters);
@@ -341,20 +339,20 @@ public void excludeUser() {
341339
TargetingFilter filter = new TargetingFilter(new TargetingFilterTestContextAccessor("Doe", null));
342340

343341
assertTrue(filter.evaluate(context));
344-
342+
345343
// Now the users is excluded
346344
Map<String, Object> excludes = new LinkedHashMap<>();
347345
Map<String, String> excludedUsers = new LinkedHashMap<>();
348346
excludedUsers.put("0", "Doe");
349-
347+
350348
excludes.put(USERS, excludedUsers);
351349
parameters.put("Exclusion", excludes);
352-
350+
353351
context.setParameters(parameters);
354-
352+
355353
assertFalse(filter.evaluate(context));
356354
}
357-
355+
358356
@Test
359357
public void excludeGroup() {
360358
FeatureFilterEvaluationContext context = new FeatureFilterEvaluationContext();
@@ -380,20 +378,20 @@ public void excludeGroup() {
380378
TargetingFilter filter = new TargetingFilter(new TargetingFilterTestContextAccessor(null, targetedGroups));
381379

382380
assertTrue(filter.evaluate(context));
383-
381+
384382
// Now the users is excluded
385383
Map<String, Object> excludes = new LinkedHashMap<>();
386384
Map<String, String> excludedGroups = new LinkedHashMap<>();
387385
excludedGroups.put("0", "g1");
388-
386+
389387
excludes.put(GROUPS, excludedGroups);
390388
parameters.put("Exclusion", excludes);
391-
389+
392390
context.setParameters(parameters);
393-
391+
394392
assertFalse(filter.evaluate(context));
395393
}
396-
394+
397395
private Map<String, Object> emptyExclusion() {
398396
Map<String, Object> excludes = new LinkedHashMap<>();
399397
List<String> excludedUsers = new ArrayList<>();
@@ -402,23 +400,4 @@ private Map<String, Object> emptyExclusion() {
402400
excludes.put(GROUPS, excludedGroups);
403401
return excludes;
404402
}
405-
406-
class TargetingFilterTestContextAccessor implements TargetingContextAccessor {
407-
408-
private String user;
409-
410-
private ArrayList<String> groups;
411-
412-
TargetingFilterTestContextAccessor(String user, ArrayList<String> groups) {
413-
this.user = user;
414-
this.groups = groups;
415-
}
416-
417-
@Override
418-
public void configureTargetingContext(TargetingContext context) {
419-
context.setUserId(user);
420-
context.setGroups(groups);
421-
}
422-
423-
}
424403
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
package com.azure.spring.cloud.feature.management.filters;
4+
5+
import com.azure.spring.cloud.feature.management.targeting.TargetingContext;
6+
import com.azure.spring.cloud.feature.management.targeting.TargetingContextAccessor;
7+
8+
import java.util.List;
9+
10+
public class TargetingFilterTestContextAccessor implements TargetingContextAccessor {
11+
12+
private String user;
13+
14+
private List<String> groups;
15+
16+
public TargetingFilterTestContextAccessor(String user, List<String> groups) {
17+
this.user = user;
18+
this.groups = groups;
19+
}
20+
21+
@Override
22+
public void configureTargetingContext(TargetingContext context) {
23+
context.setUserId(user);
24+
context.setGroups(groups);
25+
}
26+
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
package com.azure.spring.cloud.feature.management.validationstests.models;
4+
5+
public class IsEnabled {
6+
private String result;
7+
private String exception;
8+
9+
/**
10+
* @return result
11+
* */
12+
public String getResult() {
13+
return result;
14+
}
15+
16+
/**
17+
* @param result the result of validation test case
18+
* */
19+
public void setResult(String result) {
20+
this.result = result;
21+
}
22+
23+
/**
24+
* @return exception
25+
* */
26+
public String getException() {
27+
return exception;
28+
}
29+
30+
/**
31+
* @param exception the exception message throws when run test case
32+
* */
33+
public void setException(String exception) {
34+
this.exception = exception;
35+
}
36+
}

0 commit comments

Comments
 (0)