View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.configuration2.interpol;
18  
19  import static org.junit.jupiter.api.Assertions.assertEquals;
20  import static org.junit.jupiter.api.Assertions.assertFalse;
21  import static org.junit.jupiter.api.Assertions.assertNotNull;
22  import static org.junit.jupiter.api.Assertions.assertNotSame;
23  import static org.junit.jupiter.api.Assertions.assertNull;
24  import static org.junit.jupiter.api.Assertions.assertSame;
25  import static org.junit.jupiter.api.Assertions.assertThrows;
26  import static org.junit.jupiter.api.Assertions.assertTrue;
27  import static org.mockito.ArgumentMatchers.any;
28  import static org.mockito.Mockito.mock;
29  import static org.mockito.Mockito.verify;
30  import static org.mockito.Mockito.verifyNoInteractions;
31  import static org.mockito.Mockito.verifyNoMoreInteractions;
32  import static org.mockito.Mockito.when;
33  
34  import java.util.ArrayList;
35  import java.util.Arrays;
36  import java.util.Collections;
37  import java.util.EnumSet;
38  import java.util.HashMap;
39  import java.util.HashSet;
40  import java.util.Iterator;
41  import java.util.List;
42  import java.util.Map;
43  import java.util.Objects;
44  import java.util.Properties;
45  import java.util.Set;
46  import java.util.function.Function;
47  
48  import org.apache.commons.text.lookup.StringLookupFactory;
49  import org.junit.jupiter.api.BeforeEach;
50  import org.junit.jupiter.api.Test;
51  
52  /**
53   * Test class for ConfigurationInterpolator.
54   */
55  public class TestConfigurationInterpolator {
56      /** Constant for a test variable name. */
57      private static final String TEST_NAME = "varname";
58  
59      /** Constant for a test variable prefix. */
60      private static final String TEST_PREFIX = "prefix";
61  
62      /** Constant for the value of the test variable. */
63      private static final String TEST_VALUE = "TestVariableValue";
64  
65      private static void assertMappedLookups(final Map<String, Lookup> lookupMap, final String... keys) {
66          final Set<String> remainingKeys = new HashSet<>(lookupMap.keySet());
67  
68          for (final String key : keys) {
69              assertNotNull(key, "Expected map to contain string lookup for key " + key);
70  
71              remainingKeys.remove(key);
72          }
73  
74          assertEquals(Collections.emptySet(), remainingKeys);
75      }
76  
77      private static void checkDefaultPrefixLookupsHolder(final Properties props, final String... keys) {
78          final ConfigurationInterpolator.DefaultPrefixLookupsHolder holder =
79                  new ConfigurationInterpolator.DefaultPrefixLookupsHolder(props);
80  
81          final Map<String, Lookup> lookupMap = holder.getDefaultPrefixLookups();
82  
83          assertMappedLookups(lookupMap, keys);
84      }
85  
86      /**
87       * Main method used to verify the default lookups resolved during JVM execution.
88       * @param args
89       */
90      public static void main(final String[] args) {
91          System.out.println("Default lookups");
92          for (final String key : ConfigurationInterpolator.getDefaultPrefixLookups().keySet()) {
93              System.out.println("- " + key);
94          }
95      }
96  
97      /**
98       * Creates a lookup object that can resolve the test variable (and nothing else).
99       *
100      * @return the test lookup object
101      */
102     private static Lookup setUpTestLookup() {
103         return setUpTestLookup(TEST_NAME, TEST_VALUE);
104     }
105 
106     /**
107      * Creates a lookup object that can resolve the specified variable (and nothing else).
108      *
109      * @param var the variable name
110      * @param value the value of this variable
111      * @return the test lookup object
112      */
113     private static Lookup setUpTestLookup(final String var, final Object value) {
114         final Lookup lookup = mock(Lookup.class);
115         when(lookup.lookup(any())).thenAnswer(invocation -> {
116             if (var.equals(invocation.getArgument(0))) {
117                 return value;
118             }
119             return null;
120         });
121         return lookup;
122     }
123 
124     /** Stores the object to be tested. */
125     private ConfigurationInterpolator interpolator;
126 
127     @BeforeEach
128     public void setUp() throws Exception {
129         interpolator = new ConfigurationInterpolator();
130     }
131 
132     /**
133      * Tests whether multiple default lookups can be added.
134      */
135     @Test
136     void testAddDefaultLookups() {
137         final List<Lookup> lookups = new ArrayList<>();
138         lookups.add(setUpTestLookup());
139         lookups.add(setUpTestLookup("test", "value"));
140         interpolator.addDefaultLookups(lookups);
141         final List<Lookup> lookups2 = interpolator.getDefaultLookups();
142         assertEquals(lookups, lookups2);
143     }
144 
145     /**
146      * Tests whether a null collection of default lookups is handled correctly.
147      */
148     @Test
149     void testAddDefaultLookupsNull() {
150         interpolator.addDefaultLookups(null);
151         assertTrue(interpolator.getDefaultLookups().isEmpty());
152     }
153 
154     @Test
155     void testDefaultStringLookupsHolderAllLookups() {
156         final Properties props = new Properties();
157         props.setProperty(ConfigurationInterpolator.DEFAULT_PREFIX_LOOKUPS_PROPERTY,
158                 "BASE64_DECODER BASE64_ENCODER const, date, dns, environment "
159                 + "file ,java, local_host properties, resource_bundle,script,system_properties "
160                 + "url url_decoder  , url_encoder, xml");
161 
162         checkDefaultPrefixLookupsHolder(props,
163                 "base64",
164                 StringLookupFactory.KEY_BASE64_DECODER,
165                 StringLookupFactory.KEY_BASE64_ENCODER,
166                 StringLookupFactory.KEY_CONST,
167                 StringLookupFactory.KEY_DATE,
168                 StringLookupFactory.KEY_ENV,
169                 StringLookupFactory.KEY_FILE,
170                 StringLookupFactory.KEY_JAVA,
171                 StringLookupFactory.KEY_LOCALHOST,
172                 StringLookupFactory.KEY_PROPERTIES,
173                 StringLookupFactory.KEY_RESOURCE_BUNDLE,
174                 StringLookupFactory.KEY_SYS,
175                 StringLookupFactory.KEY_URL_DECODER,
176                 StringLookupFactory.KEY_URL_ENCODER,
177                 StringLookupFactory.KEY_XML,
178 
179                 StringLookupFactory.KEY_DNS,
180                 StringLookupFactory.KEY_URL,
181                 StringLookupFactory.KEY_SCRIPT);
182     }
183 
184     @Test
185     void testDefaultStringLookupsHolderGivenSingleLookup() {
186         final Properties props = new Properties();
187         props.setProperty(ConfigurationInterpolator.DEFAULT_PREFIX_LOOKUPS_PROPERTY, "base64_encoder");
188         checkDefaultPrefixLookupsHolder(props, "base64", StringLookupFactory.KEY_BASE64_ENCODER);
189     }
190 
191     @Test
192     void testDefaultStringLookupsHolderGivenSingleLookupWeirdString() {
193         final Properties props = new Properties();
194         props.setProperty(ConfigurationInterpolator.DEFAULT_PREFIX_LOOKUPS_PROPERTY, " \n \t  ,, DnS , , ");
195         checkDefaultPrefixLookupsHolder(props, StringLookupFactory.KEY_DNS);
196     }
197 
198     @Test
199     void testDefaultStringLookupsHolderInvalidLookupsDefinition() {
200         final Properties props = new Properties();
201         props.setProperty(ConfigurationInterpolator.DEFAULT_PREFIX_LOOKUPS_PROPERTY, "base64_encoder nope");
202         final Exception exc = assertThrows(Exception.class, () -> new ConfigurationInterpolator.DefaultPrefixLookupsHolder(props));
203         assertEquals("Invalid default lookups definition: base64_encoder nope", exc.getMessage());
204     }
205 
206     @Test
207     void testDefaultStringLookupsHolderLookupsPropertyEmptyAndBlank() {
208         final Properties propsWithNull = new Properties();
209         propsWithNull.setProperty(ConfigurationInterpolator.DEFAULT_PREFIX_LOOKUPS_PROPERTY, "");
210 
211         checkDefaultPrefixLookupsHolder(propsWithNull);
212 
213         final Properties propsWithBlank = new Properties();
214         propsWithBlank.setProperty(ConfigurationInterpolator.DEFAULT_PREFIX_LOOKUPS_PROPERTY, " ");
215 
216         checkDefaultPrefixLookupsHolder(propsWithBlank);
217     }
218 
219     @Test
220     void testDefaultStringLookupsHolderLookupsPropertyNotPresent() {
221         checkDefaultPrefixLookupsHolder(new Properties(),
222                 "base64",
223                 StringLookupFactory.KEY_BASE64_DECODER,
224                 StringLookupFactory.KEY_BASE64_ENCODER,
225                 StringLookupFactory.KEY_CONST,
226                 StringLookupFactory.KEY_DATE,
227                 StringLookupFactory.KEY_ENV,
228                 StringLookupFactory.KEY_FILE,
229                 StringLookupFactory.KEY_JAVA,
230                 StringLookupFactory.KEY_LOCALHOST,
231                 StringLookupFactory.KEY_PROPERTIES,
232                 StringLookupFactory.KEY_RESOURCE_BUNDLE,
233                 StringLookupFactory.KEY_SYS,
234                 StringLookupFactory.KEY_URL_DECODER,
235                 StringLookupFactory.KEY_URL_ENCODER,
236                 StringLookupFactory.KEY_XML);
237     }
238 
239     @Test
240     void testDefaultStringLookupsHolderMultipleLookups() {
241         final Properties props = new Properties();
242         props.setProperty(ConfigurationInterpolator.DEFAULT_PREFIX_LOOKUPS_PROPERTY, "dns, url script ");
243 
244         checkDefaultPrefixLookupsHolder(props,
245                 StringLookupFactory.KEY_DNS,
246                 StringLookupFactory.KEY_URL,
247                 StringLookupFactory.KEY_SCRIPT);
248     }
249 
250     /**
251      * Tests deregistering a lookup object.
252      */
253     @Test
254     void testDeregisterLookup() {
255         final Lookup lookup = mock(Lookup.class);
256         interpolator.registerLookup(TEST_PREFIX, lookup);
257         assertTrue(interpolator.deregisterLookup(TEST_PREFIX));
258         assertFalse(interpolator.prefixSet().contains(TEST_PREFIX));
259         assertTrue(interpolator.getLookups().isEmpty());
260     }
261 
262     /**
263      * Tests deregistering an unknown lookup object.
264      */
265     @Test
266     void testDeregisterLookupNonExisting() {
267         assertFalse(interpolator.deregisterLookup(TEST_PREFIX));
268     }
269 
270     /**
271      * Tests whether the flag for substitution in variable names can be modified.
272      */
273     @Test
274     void testEnableSubstitutionInVariables() {
275         assertFalse(interpolator.isEnableSubstitutionInVariables());
276         interpolator.addDefaultLookup(setUpTestLookup("java.version", "1.4"));
277         interpolator.addDefaultLookup(setUpTestLookup("jre-1.4", "C:\\java\\1.4"));
278         final String var = "${jre-${java.version}}";
279         assertEquals(var, interpolator.interpolate(var));
280         interpolator.setEnableSubstitutionInVariables(true);
281         assertTrue(interpolator.isEnableSubstitutionInVariables());
282         assertEquals("C:\\java\\1.4", interpolator.interpolate(var));
283     }
284 
285     /**
286      * Tests fromSpecification() if the specification contains an instance.
287      */
288     @Test
289     void testFromSpecificationInterpolator() {
290         final ConfigurationInterpolator ci = mock(ConfigurationInterpolator.class);
291         final InterpolatorSpecification spec = new InterpolatorSpecification.Builder().withDefaultLookup(mock(Lookup.class))
292             .withParentInterpolator(interpolator).withInterpolator(ci).create();
293         assertSame(ci, ConfigurationInterpolator.fromSpecification(spec));
294     }
295 
296     /**
297      * Tests fromSpecification() if a new instance has to be created.
298      */
299     @Test
300     void testFromSpecificationNewInstance() {
301         final Lookup defLookup = mock(Lookup.class);
302         final Lookup preLookup = mock(Lookup.class);
303         final Function<Object, String> stringConverter = obj -> Objects.toString(obj, null);
304         final InterpolatorSpecification spec = new InterpolatorSpecification.Builder()
305             .withDefaultLookup(defLookup)
306             .withPrefixLookup("p", preLookup)
307             .withParentInterpolator(interpolator)
308             .withStringConverter(stringConverter)
309             .create();
310         final ConfigurationInterpolator ci = ConfigurationInterpolator.fromSpecification(spec);
311         assertEquals(Arrays.asList(defLookup), ci.getDefaultLookups());
312         assertEquals(1, ci.getLookups().size());
313         assertSame(preLookup, ci.getLookups().get("p"));
314         assertSame(interpolator, ci.getParentInterpolator());
315         assertSame(stringConverter, ci.getStringConverter());
316     }
317 
318     /**
319      * Tries to obtain an instance from a null specification.
320      */
321     @Test
322     void testFromSpecificationNull() {
323         assertThrows(IllegalArgumentException.class, () -> ConfigurationInterpolator.fromSpecification(null));
324     }
325 
326     /**
327      * Tests whether modification of the list of default lookups does not affect the object.
328      */
329     @Test
330     void testGetDefaultLookupsModify() {
331         final List<Lookup> lookups = interpolator.getDefaultLookups();
332         lookups.add(setUpTestLookup());
333         assertTrue(interpolator.getDefaultLookups().isEmpty());
334     }
335 
336     /**
337      * Tests whether default prefix lookups can be queried as a map.
338      */
339     @Test
340     void testGetDefaultPrefixLookups() {
341         final EnumSet<DefaultLookups> excluded = EnumSet.of(
342                 DefaultLookups.DNS,
343                 DefaultLookups.URL,
344                 DefaultLookups.SCRIPT);
345 
346         final EnumSet<DefaultLookups> included = EnumSet.complementOf(excluded);
347 
348         final Map<String, Lookup> lookups = ConfigurationInterpolator.getDefaultPrefixLookups();
349 
350         assertEquals(included.size(), lookups.size());
351         for (final DefaultLookups l : included) {
352             assertSame(l.getLookup(), lookups.get(l.getPrefix()), "Wrong entry for " + l);
353         }
354 
355         for (final DefaultLookups l : excluded) {
356             assertNull(lookups.get(l.getPrefix()), "Unexpected entry for " + l);
357         }
358     }
359 
360     /**
361      * Tests that the map with default lookups cannot be modified.
362      */
363     @Test
364     void testGetDefaultPrefixLookupsModify() {
365         final Map<String, Lookup> lookups = ConfigurationInterpolator.getDefaultPrefixLookups();
366         final Lookup lookup = mock(Lookup.class);
367         assertThrows(UnsupportedOperationException.class, () -> lookups.put("test", lookup));
368 
369         verifyNoInteractions(lookup);
370     }
371 
372     /**
373      * Tests that modification of the map with lookups does not affect the object.
374      */
375     @Test
376     void testGetLookupsModify() {
377         final Map<String, Lookup> lookups = interpolator.getLookups();
378         lookups.put(TEST_PREFIX, setUpTestLookup());
379         assertTrue(interpolator.getLookups().isEmpty());
380     }
381 
382     /**
383      * Tests creating an instance. Does it contain some predefined lookups and a default string converter?
384      */
385     @Test
386     void testInit() {
387         assertTrue(interpolator.getDefaultLookups().isEmpty());
388         assertTrue(interpolator.getLookups().isEmpty());
389         assertNull(interpolator.getParentInterpolator());
390         assertNotNull(interpolator.getStringConverter());
391         assertEquals("1", interpolator.getStringConverter().apply(Arrays.asList(1, 2)));
392     }
393 
394     /**
395      * Tests interpolation of an array argument.
396      */
397     @Test
398     void testInterpolateArray() {
399         final int[] value = {1, 2};
400         assertSame(value, interpolator.interpolate(value));
401     }
402 
403     /**
404      * Tests that a blank variable definition does not cause problems.
405      */
406     @Test
407     void testInterpolateBlankVariable() {
408         final String value = "${ }";
409         assertEquals(value, interpolator.interpolate(value));
410     }
411 
412     /**
413      * Tests interpolation of a collection argument.
414      */
415     @Test
416     void testInterpolateCollection() {
417         final List<Integer> value = Arrays.asList(1, 2);
418         assertSame(value, interpolator.interpolate(value));
419     }
420 
421     /**
422      * Tests that an empty variable definition does not cause problems.
423      */
424     @Test
425     void testInterpolateEmptyVariable() {
426         final String value = "${}";
427         assertEquals(value, interpolator.interpolate(value));
428     }
429 
430     /**
431      * Tests interpolation of a non string argument.
432      */
433     @Test
434     void testInterpolateObject() {
435         final Object value = 42;
436         assertSame(value, interpolator.interpolate(value));
437     }
438 
439     /**
440      * Tests a successful interpolation of a string value.
441      */
442     @Test
443     void testInterpolateString() {
444         final String value = "${" + TEST_PREFIX + ':' + TEST_NAME + "}";
445         interpolator.registerLookup(TEST_PREFIX, setUpTestLookup());
446         assertEquals(TEST_VALUE, interpolator.interpolate(value));
447     }
448 
449     /**
450      * Tests interpolation with a variable which cannot be resolved.
451      */
452     @Test
453     void testInterpolateStringUnknownVariable() {
454         final String value = "${unknownVariable}";
455         assertEquals(value, interpolator.interpolate(value));
456     }
457 
458     /**
459      * Tests an interpolated string that begins and ends with variable lookups that have
460      * the potential to fail. Part of CONFIGURATION-764.
461      */
462     @Test
463     void testInterpolationBeginningAndEndingRiskyVariableLookups() {
464         interpolator.registerLookups(ConfigurationInterpolator.getDefaultPrefixLookups());
465         final String result = (String) interpolator.interpolate("${date:yyyy-MM}-${date:dd}");
466         assertTrue(result.matches("\\d{4}-\\d{2}-\\d{2}"));
467     }
468 
469     /**
470      * Tests interpolation with multiple variables containing arrays.
471      */
472     @Test
473     void testInterpolationMultipleArrayVariables() {
474         final String value = "${single}bc${multi}23${empty}${null}";
475         final int[] multi = {1, 0, 0};
476         final String[] single = {"a"};
477         final int[] empty = {};
478         final Object[] containsNull = {null};
479         interpolator.addDefaultLookup(setUpTestLookup("multi", multi));
480         interpolator.addDefaultLookup(setUpTestLookup("single", single));
481         interpolator.addDefaultLookup(setUpTestLookup("empty", empty));
482         interpolator.addDefaultLookup(setUpTestLookup("null", containsNull));
483         assertEquals("abc123${empty}${null}", interpolator.interpolate(value));
484     }
485 
486     /**
487      * Tests interpolation with multiple variables containing collections and iterators.
488      */
489     @Test
490     void testInterpolationMultipleCollectionVariables() {
491         final String value = "${single}bc${multi}23${empty}${null}${multiIt}${emptyIt}${nullIt}";
492         final List<Integer> multi = Arrays.asList(1, 0, 0);
493         final List<String> single = Arrays.asList("a");
494         final List<Object> empty = Collections.emptyList();
495         final List<Object> containsNull = Arrays.asList((Object) null);
496         interpolator.addDefaultLookup(setUpTestLookup("multi", multi));
497         interpolator.addDefaultLookup(setUpTestLookup("multiIt", multi.iterator()));
498         interpolator.addDefaultLookup(setUpTestLookup("single", single));
499         interpolator.addDefaultLookup(setUpTestLookup("empty", empty));
500         interpolator.addDefaultLookup(setUpTestLookup("emptyIt", empty.iterator()));
501         interpolator.addDefaultLookup(setUpTestLookup("null", containsNull));
502         interpolator.addDefaultLookup(setUpTestLookup("nullIt", containsNull.iterator()));
503         assertEquals("abc123${empty}${null}1${emptyIt}${nullIt}", interpolator.interpolate(value));
504     }
505 
506     /**
507      * Tests interpolation with variables containing multiple simple non-string variables.
508      */
509     @Test
510     void testInterpolationMultipleSimpleNonStringVariables() {
511         final String value = "${x} = ${y} is ${result}";
512         interpolator.addDefaultLookup(setUpTestLookup("x", 1));
513         interpolator.addDefaultLookup(setUpTestLookup("y", 2));
514         interpolator.addDefaultLookup(setUpTestLookup("result", false));
515         assertEquals("1 = 2 is false", interpolator.interpolate(value));
516     }
517 
518     /**
519      * Tests a property value consisting of multiple variables.
520      */
521     @Test
522     void testInterpolationMultipleVariables() {
523         final String value = "The ${subject} jumps over ${object}.";
524         interpolator.addDefaultLookup(setUpTestLookup("subject", "quick brown fox"));
525         interpolator.addDefaultLookup(setUpTestLookup("object", "the lazy dog"));
526         assertEquals("The quick brown fox jumps over the lazy dog.", interpolator.interpolate(value));
527     }
528 
529     /**
530      * Tests an interpolation that consists of a single array variable only. The variable's value
531      * should be returned verbatim.
532      */
533     @Test
534     void testInterpolationSingleArrayVariable() {
535         final int[] value = {42, -1};
536         interpolator.addDefaultLookup(setUpTestLookup(TEST_NAME, value));
537         assertEquals(value, interpolator.interpolate("${" + TEST_NAME + "}"));
538     }
539 
540     /**
541      * Tests an interpolation that consists of a single collection variable only. The variable's value
542      * should be returned verbatim.
543      */
544     @Test
545     void testInterpolationSingleCollectionVariable() {
546         final List<Integer> value = Arrays.asList(42);
547         interpolator.addDefaultLookup(setUpTestLookup(TEST_NAME, value));
548         assertEquals(value, interpolator.interpolate("${" + TEST_NAME + "}"));
549     }
550 
551     /**
552      * Tests an interpolation that consists of a single variable only. The variable's value should be returned verbatim.
553      */
554     @Test
555     void testInterpolationSingleVariable() {
556         final Object value = 42;
557         interpolator.addDefaultLookup(setUpTestLookup(TEST_NAME, value));
558         assertEquals(value, interpolator.interpolate("${" + TEST_NAME + "}"));
559     }
560 
561     /**
562      * Tests an interpolation that consists of a single undefined variable only with and without a default value.
563      */
564     @Test
565     void testInterpolationSingleVariableDefaultValue() {
566         final Object value = 42;
567         interpolator.addDefaultLookup(setUpTestLookup(TEST_NAME, value));
568         assertEquals("${I_am_not_defined}", interpolator.interpolate("${I_am_not_defined}"));
569         assertEquals("42", interpolator.interpolate("${I_am_not_defined:-42}"));
570         assertEquals("", interpolator.interpolate("${I_am_not_defined:-}"));
571     }
572 
573     /**
574      * Tests a variable declaration which lacks the trailing closing bracket.
575      */
576     @Test
577     void testInterpolationVariableIncomplete() {
578         final String value = "${" + TEST_NAME;
579         interpolator.addDefaultLookup(setUpTestLookup(TEST_NAME, "someValue"));
580         assertEquals(value, interpolator.interpolate(value));
581     }
582 
583     /**
584      * Tests nullSafeLookup() if a lookup object was provided.
585      */
586     @Test
587     void testNullSafeLookupExisting() {
588         final Lookup look = mock(Lookup.class);
589         assertSame(look, ConfigurationInterpolator.nullSafeLookup(look));
590     }
591 
592     /**
593      * Tests whether nullSafeLookup() can handle null input.
594      */
595     @Test
596     void testNullSafeLookupNull() {
597         final Lookup lookup = ConfigurationInterpolator.nullSafeLookup(null);
598         assertNull(lookup.lookup("someVar"));
599     }
600 
601     /**
602      * Tests that the prefix set cannot be modified.
603      */
604     @Test
605     void testPrefixSetModify() {
606         interpolator.registerLookup(TEST_PREFIX, setUpTestLookup());
607         final Iterator<String> it = interpolator.prefixSet().iterator();
608         it.next();
609         assertThrows(UnsupportedOperationException.class, it::remove);
610     }
611 
612     /**
613      * Tests registering a lookup object at an instance.
614      */
615     @Test
616     void testRegisterLookup() {
617         final Lookup lookup = mock(Lookup.class);
618         interpolator.registerLookup(TEST_PREFIX, lookup);
619         assertSame(lookup, interpolator.getLookups().get(TEST_PREFIX));
620         assertTrue(interpolator.prefixSet().contains(TEST_PREFIX));
621         assertTrue(interpolator.getDefaultLookups().isEmpty());
622     }
623 
624     /**
625      * Tests registering a null lookup object. This should cause an exception.
626      */
627     @Test
628     void testRegisterLookupNull() {
629         assertThrows(IllegalArgumentException.class, () -> interpolator.registerLookup(TEST_PREFIX, null));
630     }
631 
632     /**
633      * Tests registering a lookup object for an undefined prefix. This should cause an exception.
634      */
635     @Test
636     void testRegisterLookupNullPrefix() {
637         final Lookup lookup = mock(Lookup.class);
638         assertThrows(IllegalArgumentException.class, () -> interpolator.registerLookup(null, lookup));
639 
640         verifyNoInteractions(lookup);
641     }
642 
643     /**
644      * Tests whether a map with lookup objects can be registered.
645      */
646     @Test
647     void testRegisterLookups() {
648         final Lookup l1 = setUpTestLookup();
649         final Lookup l2 = setUpTestLookup("someVar", "someValue");
650         final Map<String, Lookup> lookups = new HashMap<>();
651         lookups.put(TEST_PREFIX, l1);
652         final String prefix2 = TEST_PREFIX + "_other";
653         lookups.put(prefix2, l2);
654         interpolator.registerLookups(lookups);
655         final Map<String, Lookup> lookups2 = interpolator.getLookups();
656 
657         final Map<String, Lookup> expected = new HashMap<>();
658         expected.put(TEST_PREFIX, l1);
659         expected.put(prefix2, l2);
660         assertEquals(expected, lookups2);
661     }
662 
663     /**
664      * Tests whether a null map with lookup objects is handled correctly.
665      */
666     @Test
667     void testRegisterLookupsNull() {
668         interpolator.registerLookups(null);
669         assertTrue(interpolator.getLookups().isEmpty());
670     }
671 
672     /**
673      * Tests whether a default lookup object can be removed.
674      */
675     @Test
676     void testRemoveDefaultLookup() {
677         final List<Lookup> lookups = new ArrayList<>();
678         lookups.add(setUpTestLookup());
679         lookups.add(setUpTestLookup("test", "value"));
680         interpolator.addDefaultLookups(lookups);
681         assertTrue(interpolator.removeDefaultLookup(lookups.get(0)));
682         assertFalse(interpolator.getDefaultLookups().contains(lookups.get(0)));
683         assertEquals(1, interpolator.getDefaultLookups().size());
684     }
685 
686     /**
687      * Tests whether a non existing default lookup object can be removed.
688      */
689     @Test
690     void testRemoveDefaultLookupNonExisting() {
691         assertFalse(interpolator.removeDefaultLookup(setUpTestLookup()));
692     }
693 
694     /**
695      * Tests looking up a variable without a prefix. This should trigger the default lookup object.
696      */
697     @Test
698     void testResolveDefault() {
699         final Lookup l1 = mock(Lookup.class);
700         final Lookup l2 = mock(Lookup.class);
701         final Lookup l3 = mock(Lookup.class);
702 
703         when(l1.lookup(TEST_NAME)).thenReturn(null);
704         when(l2.lookup(TEST_NAME)).thenReturn(TEST_VALUE);
705 
706         interpolator.addDefaultLookups(Arrays.asList(l1, l2, l3));
707         assertEquals(TEST_VALUE, interpolator.resolve(TEST_NAME));
708 
709         verify(l1).lookup(TEST_NAME);
710         verify(l2).lookup(TEST_NAME);
711         verifyNoMoreInteractions(l1, l2, l3);
712     }
713 
714     /**
715      * Tests whether the default lookup is called for variables with a prefix when the lookup that was registered for this
716      * prefix is not able to resolve the variable.
717      */
718     @Test
719     void testResolveDefaultAfterPrefixFails() {
720         final String varName = TEST_PREFIX + ':' + TEST_NAME + "2";
721         interpolator.registerLookup(TEST_PREFIX, setUpTestLookup());
722         interpolator.addDefaultLookup(setUpTestLookup(varName, TEST_VALUE));
723         assertEquals(TEST_VALUE, interpolator.resolve(varName));
724     }
725 
726     /**
727      * Tests an empty variable name without a prefix.
728      */
729     @Test
730     void testResolveDefaultEmptyVarName() {
731         interpolator.addDefaultLookup(setUpTestLookup("", TEST_VALUE));
732         assertEquals(TEST_VALUE, interpolator.resolve(""));
733     }
734 
735     /**
736      * Tests the empty variable prefix. This is a special case, but legal.
737      */
738     @Test
739     void testResolveEmptyPrefix() {
740         interpolator.registerLookup("", setUpTestLookup());
741         assertEquals(TEST_VALUE, interpolator.resolve(":" + TEST_NAME));
742     }
743 
744     /**
745      * Tests an empty variable name.
746      */
747     @Test
748     void testResolveEmptyVarName() {
749         interpolator.registerLookup(TEST_PREFIX, setUpTestLookup("", TEST_VALUE));
750         assertEquals(TEST_VALUE, interpolator.resolve(TEST_PREFIX + ":"));
751     }
752 
753     /**
754      * Tests looking up a variable without a prefix when no default lookup is specified. Result should be null in this case.
755      */
756     @Test
757     void testResolveNoDefault() {
758         assertNull(interpolator.resolve(TEST_NAME));
759     }
760 
761     /**
762      * Tests looking up a null variable. Result should be null, too.
763      */
764     @Test
765     void testResolveNull() {
766         assertNull(interpolator.resolve(null));
767     }
768 
769     /**
770      * Tests handling of a parent {@code ConfigurationInterpolator} if the variable can already be resolved by the current
771      * instance.
772      */
773     @Test
774     void testResolveParentVariableFound() {
775         final ConfigurationInterpolator parent = mock(ConfigurationInterpolator.class);
776         interpolator.setParentInterpolator(parent);
777         interpolator.registerLookup(TEST_PREFIX, setUpTestLookup());
778         assertEquals(TEST_VALUE, interpolator.resolve(TEST_PREFIX + ':' + TEST_NAME));
779     }
780 
781     /**
782      * Tests whether the parent {@code ConfigurationInterpolator} is invoked if the test instance cannot resolve a variable.
783      */
784     @Test
785     void testResolveParentVariableNotFound() {
786         final ConfigurationInterpolator parent = mock(ConfigurationInterpolator.class);
787 
788         when(parent.resolve(TEST_NAME)).thenReturn(TEST_VALUE);
789 
790         interpolator.setParentInterpolator(parent);
791         assertEquals(TEST_VALUE, interpolator.resolve(TEST_NAME));
792 
793         verify(parent).resolve(TEST_NAME);
794         verifyNoMoreInteractions(parent);
795     }
796 
797     /**
798      * Tests whether a variable can be resolved using the associated lookup object. The lookup is identified by the
799      * variable's prefix.
800      */
801     @Test
802     void testResolveWithPrefix() {
803         interpolator.registerLookup(TEST_PREFIX, setUpTestLookup());
804         assertEquals(TEST_VALUE, interpolator.resolve(TEST_PREFIX + ':' + TEST_NAME));
805     }
806 
807     /**
808      * Tests the behavior of the lookup method for variables with an unknown prefix. These variables should not be resolved.
809      */
810     @Test
811     void testResolveWithUnknownPrefix() {
812         interpolator.registerLookup(TEST_PREFIX, setUpTestLookup());
813         assertNull(interpolator.resolve("UnknownPrefix:" + TEST_NAME));
814         assertNull(interpolator.resolve(":" + TEST_NAME));
815     }
816 
817     /**
818      * Tests that a custom string converter can be used.
819      */
820     @Test
821     void testSetStringConverter() {
822         final Function<Object, String> stringConverter = obj -> "'" + obj + "'";
823         interpolator.addDefaultLookup(setUpTestLookup("x", Arrays.asList(1, 2)));
824         interpolator.addDefaultLookup(setUpTestLookup("y", "abc"));
825         interpolator.setStringConverter(stringConverter);
826         assertSame(stringConverter, interpolator.getStringConverter());
827         assertEquals("'abc': '[1, 2]'", interpolator.interpolate("${y}: ${x}"));
828     }
829 
830     /**
831      * Tests that the default string converter can be reapplied by passing {@code null}.
832      */
833     @Test
834     void testSetStringConverterNullArgumentUsesDefault() {
835         final Function<Object, String> stringConverter = obj -> "'" + obj + "'";
836         interpolator.addDefaultLookup(setUpTestLookup("x", Arrays.asList(1, 2)));
837         interpolator.addDefaultLookup(setUpTestLookup("y", "abc"));
838         interpolator.setStringConverter(stringConverter);
839         interpolator.setStringConverter(null);
840         assertNotSame(stringConverter, interpolator.getStringConverter());
841         assertEquals("abc: 1", interpolator.interpolate("${y}: ${x}"));
842     }
843 }