28 | * Security Note: All calls made to SSM Parameter Store decrypt SecureString parameter values and cache them in plain text in the
29 | * in-memory cache. However, this class does not write decrypted SecureString values to disk.
30 | *
31 | *
Using pathPrefix
32 | *
33 | * The client accepts a parameter path prefix, which allows it to make more efficient bulk calls to the SSM Parameter Store API
34 | * and bulk load all parameters under the given path prefix at once. For example, you might use the following hierarchy for storing
35 | * parameters for FooService:
36 | *
44 | * In this example, there are separate parameters for FooService depending on if the environment is Dev or Prod. If SsmParameterCachingClient
45 | * is passed "/FooService/Dev/" as the pathPrefix, it will make a bulk call to the SSM Parameter Store API to load and cache all
46 | * parameters that start with "/FooService/Dev/".
47 | *
48 | * Also, the get methods on the client will automatically prepend the pathPrefix so you can initialize the client once with the pathPrefix and
49 | * then reference the parameters without the prefix from there on out:
50 | *
62 | * Using this pattern, only your initialization code needs to know what environment you're running in, and the rest of your business logic can
63 | * refer to the parameters without the prefix.
64 | *
65 | * For more information on organizing parameters into hierarchies, see the
66 | * AWS Systems Manager User Guide.
67 | */
68 | @Slf4j
69 | public class SsmParameterCachingClient {
70 | private final SsmClient ssm;
71 | private final Duration ttl;
72 | private final String pathPrefix;
73 | private final boolean allowStaleValues;
74 | private final boolean bulkLoad;
75 | private final Clock clock;
76 |
77 | private volatile ConcurrentMap cache;
78 |
79 | @Value
80 | private static final class CachedParameter {
81 | private final Parameter parameter;
82 | private final Instant loadedAt;
83 | }
84 |
85 | /**
86 | * Package-private constructor for unit testing only.
87 | *
88 | * @param ssm AWS SSM client.
89 | * @param ttl The cache time-to-live (TTL), which is the duration before a cached value will be considered stale.
90 | * @param pathPrefix Parameter path prefix to prepend to all parameter names passed to get methods.
91 | * @param allowStaleValues If set to true, the client will fall back to returning stale cached values if SSM cannot be reached to refresh the cache.
92 | * @param bulkLoad If set to true and pathPrefix is non-null, the client will bulk load and cache all parameters for the given pathPrefix once the cache is stale, rather than loading them one at a time.
93 | * @param clock Clock override for unit testing.
94 | */
95 | SsmParameterCachingClient(@NonNull final SsmClient ssm, final Duration ttl, final String pathPrefix, final boolean allowStaleValues, final boolean bulkLoad, final Clock clock) {
96 | this.ssm = ssm;
97 | this.ttl = ttl;
98 | this.pathPrefix = pathPrefix;
99 | this.allowStaleValues = allowStaleValues;
100 | this.bulkLoad = bulkLoad;
101 | this.clock = clock;
102 |
103 | this.cache = new ConcurrentHashMap<>();
104 | }
105 |
106 | /**
107 | * Create a new caching client.
108 | *
109 | * @param ssm AWS SSM client.
110 | * @param ttl The cache time-to-live (TTL), which is the duration before a cached value will be considered stale.
111 | * @param pathPrefix Parameter path prefix to prepend to all parameter names passed to get methods.
112 | * @param allowStaleValues If set to true, the client will fall back to returning stale cached values if SSM cannot be reached to refresh the cache.
113 | * @param bulkLoad If set to true and pathPrefix is non-null, the client will bulk load and cache all parameters for the given pathPrefix once the cache is stale, rather than loading them one at a time.
114 | */
115 | public SsmParameterCachingClient(final SsmClient ssm, final Duration ttl, final String pathPrefix, final boolean allowStaleValues, final boolean bulkLoad) {
116 | this(ssm, ttl, pathPrefix, allowStaleValues, bulkLoad, Clock.systemUTC());
117 | }
118 |
119 | /**
120 | * Convenience constructor for creating a new caching client. Defaults to setting allowStaleValues and bulkLoad to true.
121 | *
122 | * @param ssm AWS SSM client.
123 | * @param ttl The cache time-to-live (TTL), which is the duration before a cached value will be considered stale.
124 | * @param pathPrefix Parameter path prefix to prepend to all parameter names passed to #get().
125 | */
126 | public SsmParameterCachingClient(final SsmClient ssm, final Duration ttl, final String pathPrefix) {
127 | this(ssm, ttl, pathPrefix, true, true);
128 | }
129 |
130 | /**
131 | * Convenience constructor for creating a new caching client. Defaults to setting pathPrefix to null, and allowStaleValues and bulkLoad to true.
132 | *
133 | * @param ssm AWS SSM client.
134 | * @param ttl The cache time-to-live (TTL), which is the duration before a cached value will be considered stale.
135 | */
136 | public SsmParameterCachingClient(final SsmClient ssm, final Duration ttl) {
137 | this(ssm, ttl, null);
138 | }
139 |
140 | /**
141 | * Get a parameter value.
142 | *
143 | * @param name Parameter name to get. If a pathPrefix has been supplied, it is prepended to name.
144 | * @return Full Parameter object returned by SSM client.
145 | */
146 | public Parameter get(final String name) {
147 | CachedParameter cached = getCachedParameter(name);
148 | if (isStale(cached)) {
149 | log.debug("SSM Parameter cache miss for pathPrefix={}, name={}. Attempting to load value from SSM.", pathPrefix, name);
150 | try {
151 | cached = load(name);
152 | } catch (Exception e) {
153 | // only case where we don't allow stale values is if we found out the parameter they're requesting doesn't exist
154 | if (e instanceof ParameterNotFoundException) {
155 | throw e;
156 | }
157 |
158 | if (allowStaleValues && cached != null) {
159 | log.warn(String.format("Failed to update cache from SSM for pathPrefix=%s, name=%s. Returning stale value instead.", pathPrefix, name), e);
160 | return cached.getParameter();
161 | }
162 | throw e;
163 | }
164 | } else {
165 | log.debug("SSM Parameter cache hit for pathPrefix={}, name={}. Returning cached value.", pathPrefix, name);
166 | }
167 |
168 | return cached.parameter;
169 | }
170 |
171 | private CachedParameter getCachedParameter(final String name) {
172 | String resolvedName = resolvedName(name);
173 | return cache.get(resolvedName);
174 | }
175 |
176 | private String resolvedName(final String name) {
177 | if (pathPrefix == null) {
178 | return name;
179 | }
180 | return pathPrefix + name;
181 | }
182 |
183 | private boolean isStale(CachedParameter cached) {
184 | return cached == null || Instant.now(clock).isAfter(cached.getLoadedAt().plus(ttl));
185 | }
186 |
187 | private CachedParameter load(String name) {
188 | String resolvedName = resolvedName(name);
189 | if (bulkLoad && pathPrefix != null) {
190 | return bulkLoad(resolvedName);
191 | }
192 | return loadSingleValue(resolvedName);
193 | }
194 |
195 | private CachedParameter bulkLoad(final String resolvedName) {
196 | log.debug("Bulk loading and caching all parameters for pathPrefix={} from SSM", pathPrefix);
197 | // Call GetParametersByPath API, paginate through and replace cache with new values.
198 | GetParametersByPathRequest request = GetParametersByPathRequest.builder()
199 | .path(pathPrefix)
200 | .recursive(true)
201 | .withDecryption(true)
202 | .build();
203 | GetParametersByPathIterable iterable = ssm.getParametersByPathPaginator(request);
204 |
205 | ConcurrentMap newCache = new ConcurrentHashMap<>();
206 | iterable.stream()
207 | .flatMap(r -> r.parameters().stream())
208 | .forEach(p -> newCache.put(p.name(), new CachedParameter(p, Instant.now(clock))));
209 |
210 | if (!newCache.containsKey(resolvedName)) {
211 | // if parameter they requested isn't in the results, it doesn't exist in SSM. Call SSM for that specific
212 | // parameter so the SSM not found exception is thrown back to the caller.
213 | log.debug("Bulk load results did not contain parameter {}", resolvedName);
214 | loadSingleValue(resolvedName);
215 |
216 | // shouldn't get here
217 | throw new IllegalStateException(
218 | String.format("Could not find SSM parameter via GetParametersByPath(path=%s), but found it with GetParameter(name=%s). This is probably a bug in the caching client",
219 | pathPrefix, resolvedName));
220 | }
221 |
222 | // replace cache in case previously cached values have since been removed from SSM parameter store
223 | cache = newCache;
224 | return cache.get(resolvedName);
225 | }
226 |
227 | private CachedParameter loadSingleValue(final String resolvedName) {
228 | log.debug("Loading and caching single value {} from SSM", resolvedName);
229 | GetParameterRequest request = GetParameterRequest.builder()
230 | .name(resolvedName)
231 | .withDecryption(true)
232 | .build();
233 |
234 | GetParameterResponse response = ssm.getParameter(request);
235 |
236 | CachedParameter cached = new CachedParameter(response.parameter(), Instant.now(clock));
237 | cache.put(response.parameter().name(), cached);
238 |
239 | return cached;
240 | }
241 |
242 | /**
243 | * Get a parameter value as a String.
244 | *
245 | * @param name Parameter name to get. If a pathPrefix has been supplied, it is prepended to name.
246 | * @return Parameter value as a string.
247 | */
248 | public String getAsString(final String name) {
249 | return get(name).value();
250 | }
251 |
252 | /**
253 | * Get a parameter value as a List of Strings.
254 | *
255 | * @param name Parameter name to get. If a pathPrefix has been supplied, it is prepended to name.
256 | * @return Parameter value as a list of strings. List is generated by splitting the parameter string using ',' as a delimiter.
257 | * @throws IllegalArgumentException if returned parameter is not of type StringList.
258 | */
259 | public List getAsStringList(final String name) {
260 | Parameter parameter = get(name);
261 | if (parameter.type() != ParameterType.STRING_LIST) {
262 | throw new IllegalArgumentException(String.format("Parameter %s is of type %s, expected StringList", parameter.name(), parameter.type()));
263 | }
264 |
265 | return Arrays.asList(parameter.value().split(","));
266 | }
267 | }
268 |
--------------------------------------------------------------------------------
/src/main/lombok.config:
--------------------------------------------------------------------------------
1 | # https://projectlombok.org/api/lombok/ConfigurationKeys.html
2 |
3 | # @NonNull parameters should throw IllegalArgumentException, not NullPointerException
4 | lombok.nonNull.exceptionType = IllegalArgumentException
5 |
6 | config.stopBubbling = true
--------------------------------------------------------------------------------
/src/test/java/software/amazon/serverless/ssmcachingclient/SsmParameterCachingClientTest.java:
--------------------------------------------------------------------------------
1 | package software.amazon.serverless.ssmcachingclient;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 | import static org.assertj.core.api.Assertions.assertThatThrownBy;
5 | import static org.mockito.ArgumentMatchers.any;
6 | import static org.mockito.Mockito.mock;
7 | import static org.mockito.Mockito.times;
8 | import static org.mockito.Mockito.verify;
9 | import static org.mockito.Mockito.verifyNoMoreInteractions;
10 | import static org.mockito.Mockito.when;
11 |
12 | import java.time.Clock;
13 | import java.time.Duration;
14 | import java.time.Instant;
15 | import java.util.Arrays;
16 |
17 | import org.junit.Before;
18 | import org.junit.Test;
19 | import org.mockito.Mock;
20 | import org.mockito.MockitoAnnotations;
21 |
22 | import software.amazon.awssdk.awscore.exception.AwsServiceException;
23 | import software.amazon.awssdk.services.ssm.SsmClient;
24 | import software.amazon.awssdk.services.ssm.model.GetParameterRequest;
25 | import software.amazon.awssdk.services.ssm.model.GetParameterResponse;
26 | import software.amazon.awssdk.services.ssm.model.GetParametersByPathRequest;
27 | import software.amazon.awssdk.services.ssm.model.GetParametersByPathResponse;
28 | import software.amazon.awssdk.services.ssm.model.Parameter;
29 | import software.amazon.awssdk.services.ssm.model.ParameterNotFoundException;
30 | import software.amazon.awssdk.services.ssm.model.ParameterType;
31 | import software.amazon.awssdk.services.ssm.paginators.GetParametersByPathIterable;
32 |
33 | public class SsmParameterCachingClientTest {
34 | private static final String PATH_PREFIX = "/foo/";
35 | private static final Duration TTL = Duration.ofSeconds(1);
36 |
37 | private static final String STRING_PARAMETER_KEY_SUFFIX = "string";
38 | private static final String STRING_PARAMETER_KEY = PATH_PREFIX + STRING_PARAMETER_KEY_SUFFIX;
39 | private static final String STRING_PARAMETER_VALUE = "stringvalue";
40 | private static final Parameter STRING_PARAMETER = Parameter.builder()
41 | .name(STRING_PARAMETER_KEY)
42 | .type(ParameterType.STRING)
43 | .value(STRING_PARAMETER_VALUE)
44 | .build();
45 |
46 | private static final String STRING_LIST_PARAMETER_KEY_SUFFIX = "stringlist";
47 | private static final String STRING_LIST_PARAMETER_KEY = PATH_PREFIX + STRING_LIST_PARAMETER_KEY_SUFFIX;
48 | private static final String STRING_LIST_PARAMETER_VALUE = "a,b,c";
49 | private static final Parameter STRING_LIST_PARAMETER = Parameter.builder()
50 | .name(STRING_LIST_PARAMETER_KEY)
51 | .type(ParameterType.STRING_LIST)
52 | .value(STRING_LIST_PARAMETER_VALUE)
53 | .build();
54 |
55 | private static final String NOT_FOUND_PARAMETER_KEY_SUFFIX = "not here!";
56 | private static final String NOT_FOUND_PARAMETER_KEY = PATH_PREFIX + NOT_FOUND_PARAMETER_KEY_SUFFIX;
57 |
58 | @Mock
59 | private SsmClient ssm;
60 | @Mock
61 | private GetParametersByPathIterable iterable;
62 |
63 | private SsmParameterCachingClient client;
64 |
65 | @Before
66 | public void setup() throws Exception {
67 | MockitoAnnotations.initMocks(this);
68 | mockParameters();
69 | client = new SsmParameterCachingClient(ssm, TTL, PATH_PREFIX);
70 | }
71 |
72 | private void mockParameters() {
73 | when(ssm.getParameter(
74 | GetParameterRequest.builder()
75 | .name(STRING_PARAMETER_KEY)
76 | .withDecryption(true)
77 | .build()
78 | )).thenReturn(
79 | GetParameterResponse.builder()
80 | .parameter(STRING_PARAMETER)
81 | .build()
82 | );
83 |
84 | when(ssm.getParameter(
85 | GetParameterRequest.builder()
86 | .name(STRING_LIST_PARAMETER_KEY)
87 | .withDecryption(true)
88 | .build()
89 | )).thenReturn(
90 | GetParameterResponse.builder()
91 | .parameter(STRING_LIST_PARAMETER)
92 | .build()
93 | );
94 |
95 | when(ssm.getParameter(
96 | GetParameterRequest.builder()
97 | .name(NOT_FOUND_PARAMETER_KEY)
98 | .withDecryption(true)
99 | .build()
100 | )).thenThrow(ParameterNotFoundException.class);
101 |
102 | when(ssm.getParametersByPathPaginator(any(GetParametersByPathRequest.class))).thenReturn(iterable);
103 | GetParametersByPathResponse response = GetParametersByPathResponse.builder()
104 | .parameters(STRING_PARAMETER, STRING_LIST_PARAMETER)
105 | .build();
106 | when(iterable.stream()).thenReturn(Arrays.asList(response).stream());
107 | }
108 |
109 | @Test
110 | public void new_nullSsmClient() throws Exception {
111 | assertThatThrownBy(() -> new SsmParameterCachingClient(null, TTL, PATH_PREFIX)).isInstanceOf(IllegalArgumentException.class);
112 | }
113 |
114 | @Test
115 | public void get_bulkLoad() throws Exception {
116 | assertThat(client.get(STRING_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_PARAMETER);
117 | assertThat(client.get(STRING_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_PARAMETER);
118 | assertThat(client.get(STRING_LIST_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_LIST_PARAMETER);
119 | assertThat(client.get(STRING_LIST_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_LIST_PARAMETER);
120 |
121 | GetParametersByPathRequest expected = GetParametersByPathRequest.builder()
122 | .path(PATH_PREFIX)
123 | .recursive(true)
124 | .withDecryption(true)
125 | .build();
126 |
127 | // should only have been called the first time
128 | verify(ssm, times(1)).getParametersByPathPaginator(expected);
129 |
130 | verifyNoMoreInteractions(ssm);
131 | }
132 |
133 | @Test
134 | public void get_bulkLoad_parameterNotFound() throws Exception {
135 | assertThatThrownBy(() -> client.get(NOT_FOUND_PARAMETER_KEY_SUFFIX)).isInstanceOf(ParameterNotFoundException.class);
136 |
137 | GetParametersByPathRequest expectedBulkRequest = GetParametersByPathRequest.builder()
138 | .path(PATH_PREFIX)
139 | .recursive(true)
140 | .withDecryption(true)
141 | .build();
142 | verify(ssm).getParametersByPathPaginator(expectedBulkRequest);
143 |
144 | GetParameterRequest expectedSingleRequest = GetParameterRequest.builder()
145 | .name(NOT_FOUND_PARAMETER_KEY)
146 | .withDecryption(true)
147 | .build();
148 | verify(ssm).getParameter(expectedSingleRequest);
149 |
150 | verifyNoMoreInteractions(ssm);
151 | }
152 |
153 | @Test
154 | public void get_bulkLoad_noPathPrefix() throws Exception {
155 | client = new SsmParameterCachingClient(ssm, TTL);
156 |
157 | assertThat(client.get(STRING_PARAMETER_KEY)).isEqualTo(STRING_PARAMETER);
158 | assertThat(client.get(STRING_PARAMETER_KEY)).isEqualTo(STRING_PARAMETER);
159 | assertThat(client.get(STRING_LIST_PARAMETER_KEY)).isEqualTo(STRING_LIST_PARAMETER);
160 | assertThat(client.get(STRING_LIST_PARAMETER_KEY)).isEqualTo(STRING_LIST_PARAMETER);
161 |
162 | GetParameterRequest expectedStringParameterRequest = GetParameterRequest.builder()
163 | .name(STRING_PARAMETER_KEY)
164 | .withDecryption(true)
165 | .build();
166 | verify(ssm, times(1)).getParameter(expectedStringParameterRequest);
167 | GetParameterRequest expectedStringListParameterRequest = GetParameterRequest.builder()
168 | .name(STRING_LIST_PARAMETER_KEY)
169 | .withDecryption(true)
170 | .build();
171 | verify(ssm, times(1)).getParameter(expectedStringListParameterRequest);
172 |
173 | verifyNoMoreInteractions(ssm);
174 | }
175 |
176 | @Test
177 | public void get_noBulkLoad() throws Exception {
178 | client = new SsmParameterCachingClient(ssm, TTL, PATH_PREFIX, true, false);
179 |
180 | assertThat(client.get(STRING_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_PARAMETER);
181 | assertThat(client.get(STRING_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_PARAMETER);
182 | assertThat(client.get(STRING_LIST_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_LIST_PARAMETER);
183 | assertThat(client.get(STRING_LIST_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_LIST_PARAMETER);
184 |
185 | GetParameterRequest expectedStringParameterRequest = GetParameterRequest.builder()
186 | .name(STRING_PARAMETER_KEY)
187 | .withDecryption(true)
188 | .build();
189 | verify(ssm, times(1)).getParameter(expectedStringParameterRequest);
190 | GetParameterRequest expectedStringListParameterRequest = GetParameterRequest.builder()
191 | .name(STRING_LIST_PARAMETER_KEY)
192 | .withDecryption(true)
193 | .build();
194 | verify(ssm, times(1)).getParameter(expectedStringListParameterRequest);
195 |
196 | verifyNoMoreInteractions(ssm);
197 | }
198 |
199 | @Test
200 | public void get_noBulkLoad_parameterNotFound() throws Exception {
201 | client = new SsmParameterCachingClient(ssm, TTL, PATH_PREFIX, true, false);
202 |
203 | assertThatThrownBy(() -> client.get(NOT_FOUND_PARAMETER_KEY_SUFFIX)).isInstanceOf(ParameterNotFoundException.class);
204 |
205 | GetParameterRequest expected = GetParameterRequest.builder()
206 | .name(NOT_FOUND_PARAMETER_KEY)
207 | .withDecryption(true)
208 | .build();
209 | verify(ssm).getParameter(expected);
210 |
211 | verifyNoMoreInteractions(ssm);
212 | }
213 |
214 | @Test
215 | public void get_staleCache_ssmSuccess() throws Exception {
216 | Clock clock = mock(Clock.class);
217 | client = new SsmParameterCachingClient(ssm, TTL, PATH_PREFIX, true, true, clock);
218 |
219 | Instant now = Instant.now();
220 | when(clock.instant()).thenReturn(now);
221 | assertThat(client.get(STRING_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_PARAMETER);
222 |
223 | when(clock.instant()).thenReturn(now.plus(Duration.ofSeconds(TTL.getSeconds() + 1)));
224 | assertThat(client.get(STRING_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_PARAMETER);
225 |
226 | GetParametersByPathRequest expected = GetParametersByPathRequest.builder()
227 | .path(PATH_PREFIX)
228 | .recursive(true)
229 | .withDecryption(true)
230 | .build();
231 |
232 | verify(ssm, times(2)).getParametersByPathPaginator(expected);
233 |
234 | verifyNoMoreInteractions(ssm);
235 | }
236 |
237 | @Test
238 | public void get_staleCache_ssmParameterNotFound() throws Exception {
239 | Clock clock = mock(Clock.class);
240 | client = new SsmParameterCachingClient(ssm, TTL, PATH_PREFIX, true, false, clock);
241 |
242 | Instant now = Instant.now();
243 | when(clock.instant()).thenReturn(now);
244 | assertThat(client.get(STRING_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_PARAMETER);
245 |
246 | when(clock.instant()).thenReturn(now.plus(Duration.ofSeconds(TTL.getSeconds() + 1)));
247 | when(ssm.getParameter(
248 | GetParameterRequest.builder()
249 | .name(STRING_PARAMETER_KEY)
250 | .withDecryption(true)
251 | .build()
252 | )).thenThrow(ParameterNotFoundException.class);
253 | assertThatThrownBy(() -> client.get(STRING_PARAMETER_KEY_SUFFIX)).isInstanceOf(ParameterNotFoundException.class);
254 | }
255 |
256 | @Test
257 | public void get_staleCache_ssmOtherException_allowStaleValues() throws Exception {
258 | Clock clock = mock(Clock.class);
259 | client = new SsmParameterCachingClient(ssm, TTL, PATH_PREFIX, true, false, clock);
260 |
261 | Instant now = Instant.now();
262 | when(clock.instant()).thenReturn(now);
263 | assertThat(client.get(STRING_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_PARAMETER);
264 |
265 | when(clock.instant()).thenReturn(now.plus(Duration.ofSeconds(TTL.getSeconds() + 1)));
266 | when(ssm.getParameter(
267 | GetParameterRequest.builder()
268 | .name(STRING_PARAMETER_KEY)
269 | .withDecryption(true)
270 | .build()
271 | )).thenThrow(AwsServiceException.class);
272 |
273 | assertThat(client.get(STRING_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_PARAMETER);
274 | }
275 |
276 | @Test
277 | public void get_staleCache_ssmOtherException_noAllowStaleValues() throws Exception {
278 | Clock clock = mock(Clock.class);
279 | client = new SsmParameterCachingClient(ssm, TTL, PATH_PREFIX, false, false, clock);
280 |
281 | Instant now = Instant.now();
282 | when(clock.instant()).thenReturn(now);
283 | assertThat(client.get(STRING_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_PARAMETER);
284 |
285 | when(clock.instant()).thenReturn(now.plus(Duration.ofSeconds(TTL.getSeconds() + 1)));
286 | when(ssm.getParameter(
287 | GetParameterRequest.builder()
288 | .name(STRING_PARAMETER_KEY)
289 | .withDecryption(true)
290 | .build()
291 | )).thenThrow(new RuntimeException("boom!"));
292 |
293 | assertThatThrownBy(() -> client.get(STRING_PARAMETER_KEY_SUFFIX))
294 | .isInstanceOf(RuntimeException.class)
295 | .hasMessage("boom!");
296 | }
297 |
298 | @Test
299 | public void getAsString() throws Exception {
300 | assertThat(client.getAsString(STRING_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_PARAMETER_VALUE);
301 | assertThat(client.getAsString(STRING_LIST_PARAMETER_KEY_SUFFIX)).isEqualTo(STRING_LIST_PARAMETER_VALUE);
302 | }
303 |
304 | @Test
305 | public void getAsStringList() throws Exception {
306 | assertThat(client.getAsStringList(STRING_LIST_PARAMETER_KEY_SUFFIX)).isEqualTo(Arrays.asList("a", "b", "c"));
307 | }
308 |
309 | @Test
310 | public void getAsStringList_wrongType() throws Exception {
311 | assertThatThrownBy(() -> client.getAsStringList(STRING_PARAMETER_KEY_SUFFIX)).isInstanceOf(IllegalArgumentException.class);
312 | }
313 | }
--------------------------------------------------------------------------------
/src/test/resources/log4j2-test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------