From a27b341714e7d1e677dac8b568c55be8bffb2b3f Mon Sep 17 00:00:00 2001 From: erabii Date: Thu, 1 Feb 2024 23:13:40 +0200 Subject: [PATCH] Adjust configmap priority order (#1566) Fixes #1564 --- .../main/asciidoc/property-source-config.adoc | 28 ++ .../config/SourceDataEntriesProcessor.java | 90 +++++-- ...ntriesProcessorOrderedPropertiesTests.java | 182 +++++++++++++ ...SourceDataEntriesProcessorSortedTests.java | 246 ++++++++++++++++++ 4 files changed, 519 insertions(+), 27 deletions(-) create mode 100644 spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/SourceDataEntriesProcessorOrderedPropertiesTests.java create mode 100644 spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/SourceDataEntriesProcessorSortedTests.java diff --git a/docs/src/main/asciidoc/property-source-config.adoc b/docs/src/main/asciidoc/property-source-config.adoc index 0e598259..a08aa920 100644 --- a/docs/src/main/asciidoc/property-source-config.adoc +++ b/docs/src/main/asciidoc/property-source-config.adoc @@ -103,6 +103,34 @@ This is what we will end-up loading: - `not-my-app.yaml` _ignored_, since it does not match `spring.application.name` - `someProp: someValue` plain property +The order of loading properties is a as follows: + +- first load all properties from `my-app.yaml` +- then all from profile-based sources: `my-app-k8s.yaml` +- then all plain properties `someProp: someValue` + +This means that profile based sources take precedence over non-profile based sources (just like in a vanilla Spring app); and plain properties take precedence over both profile and non-profile based sources. Here is an example: + +==== +[source] +---- +kind: ConfigMap +apiVersion: v1 +metadata: + name: my-app +data: + my-app-k8s.yaml: |- + key1=valueA + key2=valueB + my-app.yaml: |- + key1=valueC + key2=valueA + key1: valueD +---- +==== + +After processing such a ConfigMap, this is what you will get in the properties: `key1=valueD`, `key2=valueB`. + The single exception to the aforementioned flow is when the `ConfigMap` contains a *single* key that indicates the file is a YAML or properties file. In that case, the name of the key does NOT have to be `application.yaml` or `application.properties` (it can be anything) and the value of the property is treated correctly. diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SourceDataEntriesProcessor.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SourceDataEntriesProcessor.java index b3faabd7..0e59fe5d 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SourceDataEntriesProcessor.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SourceDataEntriesProcessor.java @@ -16,12 +16,14 @@ package org.springframework.cloud.kubernetes.commons.config; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.commons.logging.Log; @@ -32,7 +34,6 @@ import org.springframework.core.env.MapPropertySource; import static org.springframework.cloud.kubernetes.commons.config.PropertySourceUtils.KEY_VALUE_TO_PROPERTIES; import static org.springframework.cloud.kubernetes.commons.config.PropertySourceUtils.PROPERTIES_TO_MAP; -import static org.springframework.cloud.kubernetes.commons.config.PropertySourceUtils.throwingMerger; import static org.springframework.cloud.kubernetes.commons.config.PropertySourceUtils.yamlParserGenerator; /** @@ -73,42 +74,77 @@ public class SourceDataEntriesProcessor extends MapPropertySource { return defaultProcessAllEntries(input, environment); } - private static Map defaultProcessAllEntries(Map input, Environment environment) { + /** + *
+	 * 		we want to sort entries coming from the k8s source in a specific way:
+	 *
+	 * 	    1. "application.yaml/yml/properties" have to come first
+	 * 	       (or the value from spring.application.name)
+	 * 	    2. then profile specific entries, like "application-dev.yaml"
+	 * 	    3. then plain properties
+	 * 
+ */ + static List> sorted(Map input, Environment environment) { + + record WeightedEntry(Map.Entry entry, int weight) { + + } // we pass empty Strings on purpose, the logic here is either the value of - // "spring.application.name" - // or literal "application". + // "spring.application.name" or literal "application". String applicationName = ConfigUtils.getApplicationName(environment, "", ""); String[] activeProfiles = environment.getActiveProfiles(); - Set fileNames = Stream - .concat(Stream.of(applicationName), - Arrays.stream(activeProfiles).map(profile -> applicationName + "-" + profile)) - .collect(Collectors.toSet()); + // the order here is important, first has to come "application.yaml" and then + // "application-dev.yaml" + List orderedFileNames = Stream.concat(Stream.of(applicationName), + Arrays.stream(activeProfiles).map(profile -> applicationName + "-" + profile)).toList(); - return input.entrySet().stream().map(e -> extractProperties(e.getKey(), e.getValue(), fileNames, environment)) - .flatMap(m -> m.entrySet().stream()) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, throwingMerger(), HashMap::new)); - } - - private static Map extractProperties(String resourceName, String content, Set fileNames, - Environment environment) { - - if (resourceName.endsWith(".yml") || resourceName.endsWith(".yaml") || resourceName.endsWith(".properties")) { - - if (fileNames.contains(resourceName.split("\\.", 2)[0])) { - if (resourceName.endsWith(".properties")) { - LOG.debug("entry : " + resourceName + " will be treated as a single properties file"); - return KEY_VALUE_TO_PROPERTIES.andThen(PROPERTIES_TO_MAP).apply(content); + int current = orderedFileNames.size() - 1; + List weightedEntries = new ArrayList<>(); + for (Map.Entry entry : input.entrySet()) { + String key = entry.getKey(); + if (key.endsWith(".yml") || key.endsWith(".yaml") || key.endsWith(".properties")) { + String withoutExtension = key.split("\\.", 2)[0]; + int index = orderedFileNames.indexOf(withoutExtension); + if (index >= 0) { + weightedEntries.add(new WeightedEntry(entry, index)); } else { - LOG.debug("entry : " + resourceName + " will be treated as a single yml/yaml file"); - return yamlParserGenerator(environment).andThen(PROPERTIES_TO_MAP).apply(content); + LOG.warn("entry : " + key + " will be skipped"); } } else { - LOG.warn("entry : " + resourceName + " will be skipped"); - return Collections.emptyMap(); + weightedEntries.add(new WeightedEntry(entry, ++current)); + } + } + + return weightedEntries.stream().sorted(Comparator.comparing(WeightedEntry::weight)).map(WeightedEntry::entry) + .toList(); + } + + private static Map defaultProcessAllEntries(Map input, Environment environment) { + + List> sortedEntries = sorted(input, environment); + Map result = new HashMap<>(); + for (Map.Entry entry : sortedEntries) { + result.putAll(extractProperties(entry.getKey(), entry.getValue(), environment)); + } + return result; + + } + + private static Map extractProperties(String resourceName, String content, Environment environment) { + + if (resourceName.endsWith(".yml") || resourceName.endsWith(".yaml") || resourceName.endsWith(".properties")) { + + if (resourceName.endsWith(".properties")) { + LOG.debug("entry : " + resourceName + " will be treated as a single properties file"); + return KEY_VALUE_TO_PROPERTIES.andThen(PROPERTIES_TO_MAP).apply(content); + } + else { + LOG.debug("entry : " + resourceName + " will be treated as a single yml/yaml file"); + return yamlParserGenerator(environment).andThen(PROPERTIES_TO_MAP).apply(content); } } diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/SourceDataEntriesProcessorOrderedPropertiesTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/SourceDataEntriesProcessorOrderedPropertiesTests.java new file mode 100644 index 00000000..f902fa15 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/SourceDataEntriesProcessorOrderedPropertiesTests.java @@ -0,0 +1,182 @@ +/* + * Copyright 2013-2024 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 org.springframework.cloud.kubernetes.commons.config; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.mock.env.MockEnvironment; + +/** + * @author wind57 + */ +class SourceDataEntriesProcessorOrderedPropertiesTests { + + /** + *
+	 *     - a single property is present
+	 * 
+ */ + @Test + void testSingleNonFileProperty() { + Map map = new LinkedHashMap<>(); + map.put("my-key", "my-value"); + + MockEnvironment mockEnvironment = new MockEnvironment(); + Map result = SourceDataEntriesProcessor.processAllEntries(map, mockEnvironment); + + Assertions.assertEquals(Map.of("my-key", "my-value"), result); + } + + /** + *
+	 *     - a single property from a properties file
+	 * 
+ */ + @Test + void testSingleFileProperty() { + Map map = new LinkedHashMap<>(); + map.put("application.properties", "my-key=from-app"); + + MockEnvironment mockEnvironment = new MockEnvironment(); + Map result = SourceDataEntriesProcessor.processAllEntries(map, mockEnvironment); + + Assertions.assertEquals(Map.of("my-key", "from-app"), result); + } + + /** + *
+	 *     - application.properties contains:
+	 *     	{
+	 *     	    firstKey=firstFromProperties
+	 *     	    secondKey=secondFromProperties
+	 *     	}
+	 *
+	 *     	- a single property exists : {firstKey = abc}
+	 *
+	 *     	- This proves that the property overrides the value from "application.properties".
+	 * 
+ */ + @Test + void testThree() { + + Map map = new LinkedHashMap<>(); + map.put("application.properties", """ + firstKey=firstFromProperties + secondKey=secondFromProperties"""); + map.put("firstKey", "abc"); + + MockEnvironment mockEnvironment = new MockEnvironment(); + Map result = SourceDataEntriesProcessor.processAllEntries(map, mockEnvironment); + + Assertions.assertEquals(Map.of("firstKey", "abc", "secondKey", "secondFromProperties"), result); + } + + /** + *
+	 *     - application.properties contains:
+	 *     	{
+	 *     	    firstKey=firstFromProperties
+	 *     	    secondKey=secondFromProperties
+	 *     	    thirdKey=thirdFromProperties
+	 *     	}
+	 *
+	 *     	- application-dev.properties contains:
+	 *     	  {
+	 *     	  	  firstKey=firstFromDevProperties
+	 *     	      secondKey=secondFromDevProperties
+	 *     	  }
+	 *
+	 *     	- a single property exists : {firstKey = abc}
+	 *
+	 *     	- This proves that profile specific properties override non-profile
+	 *     	  and plain properties override everything.
+	 * 
+ */ + @Test + void testFour() { + + Map map = new LinkedHashMap<>(); + map.put("application.properties", """ + firstKey=firstFromProperties + secondKey=secondFromProperties + thirdKey=thirdFromProperties"""); + map.put("application-dev.properties", """ + firstKey=firstFromDevProperties + secondKey=secondFromDevProperties"""); + map.put("firstKey", "abc"); + + MockEnvironment mockEnvironment = new MockEnvironment(); + mockEnvironment.setActiveProfiles("dev"); + Map result = SourceDataEntriesProcessor.processAllEntries(map, mockEnvironment); + + Assertions.assertEquals( + Map.of("firstKey", "abc", "secondKey", "secondFromDevProperties", "thirdKey", "thirdFromProperties"), + result); + } + + /** + *
+	 *     - application.properties contains:
+	 *     	{
+	 *     	    firstKey=firstFromProperties
+	 *     	    secondKey=secondFromProperties
+	 *     	    thirdKey=thirdFromProperties
+	 *     	}
+	 *
+	 *     	- application-dev.properties contains:
+	 *     	  {
+	 *     	  	  firstKey=firstFromDevProperties
+	 *     	      secondKey=secondFromDevProperties
+	 *     	  }
+	 *
+	 *     	- a single property exists : {firstKey = abc}
+	 *
+	 *     	- This proves that profile specific properties override non-profile
+	 *     	  and plain properties override everything.
+	 *     	  It also proves that non-active profile properties are ignored.
+	 * 
+ */ + @Test + void testFive() { + + Map map = new LinkedHashMap<>(); + map.put("application.properties", """ + firstKey=firstFromProperties + secondKey=secondFromProperties + thirdKey=thirdFromProperties"""); + map.put("application-dev.properties", """ + firstKey=firstFromDevProperties + secondKey=secondFromDevProperties"""); + map.put("application-k8s.properties", """ + firstKey=firstFromK8sProperties + secondKey=secondFromK8sProperties"""); + map.put("firstKey", "abc"); + map.put("fourthKey", "def"); + + MockEnvironment mockEnvironment = new MockEnvironment(); + mockEnvironment.setActiveProfiles("dev"); + Map result = SourceDataEntriesProcessor.processAllEntries(map, mockEnvironment); + + Assertions.assertEquals(Map.of("firstKey", "abc", "secondKey", "secondFromDevProperties", "thirdKey", + "thirdFromProperties", "fourthKey", "def"), result); + } + +} diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/SourceDataEntriesProcessorSortedTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/SourceDataEntriesProcessorSortedTests.java new file mode 100644 index 00000000..34106fc9 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/SourceDataEntriesProcessorSortedTests.java @@ -0,0 +1,246 @@ +/* + * Copyright 2013-2024 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 org.springframework.cloud.kubernetes.commons.config; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.mock.env.MockEnvironment; + +/** + * @author wind57 + */ +class SourceDataEntriesProcessorSortedTests { + + @Test + void testSingleNonFileProperty() { + + Map k8sSource = new LinkedHashMap<>(); + k8sSource.put("simple-property", "value"); + + MockEnvironment mockEnvironment = new MockEnvironment(); + + List> result = SourceDataEntriesProcessor.sorted(k8sSource, mockEnvironment); + Assertions.assertEquals(result.size(), 1); + Assertions.assertEquals(result.get(0).getKey(), "simple-property"); + Assertions.assertEquals(result.get(0).getValue(), "value"); + } + + @Test + void testTwoNonFileProperties() { + + Map k8sSource = new LinkedHashMap<>(); + k8sSource.put("one", "1"); + k8sSource.put("two", "2"); + + MockEnvironment mockEnvironment = new MockEnvironment(); + + List> result = SourceDataEntriesProcessor.sorted(k8sSource, mockEnvironment); + Assertions.assertEquals(result.size(), 2); + Assertions.assertEquals(result.get(0).getKey(), "one"); + Assertions.assertEquals(result.get(0).getValue(), "1"); + + Assertions.assertEquals(result.get(1).getKey(), "two"); + Assertions.assertEquals(result.get(1).getValue(), "2"); + } + + @Test + void testSingleFileProperty() { + + Map k8sSource = new LinkedHashMap<>(); + k8sSource.put("application.properties", "key=value"); + + MockEnvironment mockEnvironment = new MockEnvironment(); + + List> result = SourceDataEntriesProcessor.sorted(k8sSource, mockEnvironment); + Assertions.assertEquals(result.size(), 1); + Assertions.assertEquals(result.get(0).getKey(), "application.properties"); + Assertions.assertEquals(result.get(0).getValue(), "key=value"); + } + + @Test + void testApplicationAndSimpleProperty() { + + Map k8sSource = new LinkedHashMap<>(); + k8sSource.put("application.properties", "key=value"); + k8sSource.put("simple", "other_value"); + + MockEnvironment mockEnvironment = new MockEnvironment(); + + List> result = SourceDataEntriesProcessor.sorted(k8sSource, mockEnvironment); + Assertions.assertEquals(result.size(), 2); + Assertions.assertEquals(result.get(0).getKey(), "application.properties"); + Assertions.assertEquals(result.get(0).getValue(), "key=value"); + + Assertions.assertEquals(result.get(1).getKey(), "simple"); + Assertions.assertEquals(result.get(1).getValue(), "other_value"); + } + + @Test + void testSimplePropertyAndApplication() { + + Map k8sSource = new LinkedHashMap<>(); + k8sSource.put("simple", "other_value"); + k8sSource.put("application.properties", "key=value"); + + MockEnvironment mockEnvironment = new MockEnvironment(); + + List> result = SourceDataEntriesProcessor.sorted(k8sSource, mockEnvironment); + Assertions.assertEquals(result.size(), 2); + Assertions.assertEquals(result.get(0).getKey(), "application.properties"); + Assertions.assertEquals(result.get(0).getValue(), "key=value"); + + Assertions.assertEquals(result.get(1).getKey(), "simple"); + Assertions.assertEquals(result.get(1).getValue(), "other_value"); + } + + @Test + void testSimplePropertyAndTwoApplications() { + + Map k8sSource = new LinkedHashMap<>(); + k8sSource.put("simple", "other_value"); + k8sSource.put("application.properties", "key=value"); + k8sSource.put("application-dev.properties", "key-dev=value-dev"); + + MockEnvironment mockEnvironment = new MockEnvironment(); + mockEnvironment.setActiveProfiles("dev"); + + List> result = SourceDataEntriesProcessor.sorted(k8sSource, mockEnvironment); + Assertions.assertEquals(result.size(), 3); + Assertions.assertEquals(result.get(0).getKey(), "application.properties"); + Assertions.assertEquals(result.get(0).getValue(), "key=value"); + + Assertions.assertEquals(result.get(1).getKey(), "application-dev.properties"); + Assertions.assertEquals(result.get(1).getValue(), "key-dev=value-dev"); + + Assertions.assertEquals(result.get(2).getKey(), "simple"); + Assertions.assertEquals(result.get(2).getValue(), "other_value"); + } + + @Test + void testComplex() { + + Map k8sSource = new LinkedHashMap<>(); + k8sSource.put("simple", "other_value"); + k8sSource.put("second-simple", "second_other_value"); + k8sSource.put("application.properties", "key=value"); + k8sSource.put("application-dev.properties", "key-dev=value-dev"); + k8sSource.put("application-k8s.properties", "key-k8s=value-k8s"); + k8sSource.put("ignored.properties", "key-ignored=value-ignored"); + + MockEnvironment mockEnvironment = new MockEnvironment(); + mockEnvironment.setActiveProfiles("k8s"); + + List> result = SourceDataEntriesProcessor.sorted(k8sSource, mockEnvironment); + Assertions.assertEquals(result.size(), 4); + Assertions.assertEquals(result.get(0).getKey(), "application.properties"); + Assertions.assertEquals(result.get(0).getValue(), "key=value"); + + Assertions.assertEquals(result.get(1).getKey(), "application-k8s.properties"); + Assertions.assertEquals(result.get(1).getValue(), "key-k8s=value-k8s"); + + Assertions.assertEquals(result.get(2).getKey(), "simple"); + Assertions.assertEquals(result.get(2).getValue(), "other_value"); + + Assertions.assertEquals(result.get(3).getKey(), "second-simple"); + Assertions.assertEquals(result.get(3).getValue(), "second_other_value"); + } + + @Test + void testComplexWithNonDefaultApplicationName() { + + Map k8sSource = new LinkedHashMap<>(); + k8sSource.put("simple", "other_value"); + k8sSource.put("second-simple", "second_other_value"); + k8sSource.put("application.properties", "key=value"); + k8sSource.put("application-dev.properties", "key-dev=value-dev"); + k8sSource.put("application-k8s.properties", "key-k8s=value-k8s"); + k8sSource.put("ignored.properties", "key-ignored=value-ignored"); + + MockEnvironment mockEnvironment = new MockEnvironment(); + mockEnvironment.setProperty("spring.application.name", "sorted"); + mockEnvironment.setActiveProfiles("k8s"); + + List> result = SourceDataEntriesProcessor.sorted(k8sSource, mockEnvironment); + Assertions.assertEquals(result.size(), 2); + + Assertions.assertEquals(result.get(0).getKey(), "simple"); + Assertions.assertEquals(result.get(0).getValue(), "other_value"); + + Assertions.assertEquals(result.get(1).getKey(), "second-simple"); + Assertions.assertEquals(result.get(1).getValue(), "second_other_value"); + } + + @Test + void testComplexWithNonDefaultApplicationNameMoreMatches() { + + Map k8sSource = new LinkedHashMap<>(); + k8sSource.put("simple", "other_value"); + k8sSource.put("second-simple", "second_other_value"); + k8sSource.put("sorted.properties", "key=value"); + k8sSource.put("application-dev.properties", "key-dev=value-dev"); + k8sSource.put("sorted-k8s.properties", "key-k8s=value-k8s"); + k8sSource.put("ignored.properties", "key-ignored=value-ignored"); + + MockEnvironment mockEnvironment = new MockEnvironment(); + mockEnvironment.setProperty("spring.application.name", "sorted"); + mockEnvironment.setActiveProfiles("k8s"); + + List> result = SourceDataEntriesProcessor.sorted(k8sSource, mockEnvironment); + Assertions.assertEquals(result.size(), 4); + Assertions.assertEquals(result.get(0).getKey(), "sorted.properties"); + Assertions.assertEquals(result.get(0).getValue(), "key=value"); + + Assertions.assertEquals(result.get(1).getKey(), "sorted-k8s.properties"); + Assertions.assertEquals(result.get(1).getValue(), "key-k8s=value-k8s"); + + Assertions.assertEquals(result.get(2).getKey(), "simple"); + Assertions.assertEquals(result.get(2).getValue(), "other_value"); + + Assertions.assertEquals(result.get(3).getKey(), "second-simple"); + Assertions.assertEquals(result.get(3).getValue(), "second_other_value"); + } + + @Test + void testProfileBasedOnly() { + Map k8sSource = new LinkedHashMap<>(); + k8sSource.put("simple", "other_value"); + k8sSource.put("second-simple", "second_other_value"); + k8sSource.put("sorted-k8s.properties", "key-k8s=value-k8s"); + k8sSource.put("ignored.properties", "key-ignored=value-ignored"); + + MockEnvironment mockEnvironment = new MockEnvironment(); + mockEnvironment.setProperty("spring.application.name", "sorted"); + mockEnvironment.setActiveProfiles("k8s"); + + List> result = SourceDataEntriesProcessor.sorted(k8sSource, mockEnvironment); + Assertions.assertEquals(result.size(), 3); + Assertions.assertEquals(result.get(0).getKey(), "sorted-k8s.properties"); + Assertions.assertEquals(result.get(0).getValue(), "key-k8s=value-k8s"); + + Assertions.assertEquals(result.get(1).getKey(), "simple"); + Assertions.assertEquals(result.get(1).getValue(), "other_value"); + + Assertions.assertEquals(result.get(2).getKey(), "second-simple"); + Assertions.assertEquals(result.get(2).getValue(), "second_other_value"); + } + +}