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