• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2017 Google Inc.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //      http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 //
15 ////////////////////////////////////////////////////////////////////////////////
16 
17 package com.google.crypto.tink.util;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertThrows;
21 import static org.junit.Assert.assertTrue;
22 
23 import com.google.api.client.http.HttpStatusCodes;
24 import com.google.api.client.http.HttpTransport;
25 import com.google.api.client.http.LowLevelHttpRequest;
26 import com.google.api.client.testing.http.MockHttpTransport;
27 import com.google.api.client.testing.http.MockLowLevelHttpRequest;
28 import com.google.api.client.testing.http.MockLowLevelHttpResponse;
29 import com.google.errorprone.annotations.CanIgnoreReturnValue;
30 import java.io.IOException;
31 import java.util.ArrayList;
32 import java.util.List;
33 import java.util.concurrent.Callable;
34 import java.util.concurrent.CountDownLatch;
35 import java.util.concurrent.Executor;
36 import java.util.concurrent.ExecutorService;
37 import java.util.concurrent.Executors;
38 import java.util.concurrent.FutureTask;
39 import java.util.concurrent.RejectedExecutionException;
40 import java.util.concurrent.TimeUnit;
41 import java.util.concurrent.atomic.AtomicInteger;
42 import org.junit.After;
43 import org.junit.Before;
44 import org.junit.Test;
45 import org.junit.runner.RunWith;
46 import org.junit.runners.JUnit4;
47 
48 /** Tests for {@link KeysDownloader}. */
49 @RunWith(JUnit4.class)
50 public class KeysDownloaderTest {
51   private static final long INITIAL_CURRENT_TIME_IN_MILLIS = 1000;
52 
53   private CountDownLatch backgroundFetchFinishedLatch;
54   private CountDownLatch delayHttpResponseLatch;
55   private ExecutorService executor;
56   private HttpResponseBuilder httpResponseBuilder;
57   private AtomicInteger backgroundFetchStartedCount;
58   private AtomicInteger httpTransportGetCount;
59   private boolean executorIsAcceptingRunnables;
60   private long currentTimeInMillis;
61 
62   @Before
setUp()63   public void setUp() {
64     backgroundFetchFinishedLatch = new CountDownLatch(1);
65     delayHttpResponseLatch = null;
66     executor = Executors.newCachedThreadPool();
67     httpResponseBuilder = new HttpResponseBuilder();
68     backgroundFetchStartedCount = new AtomicInteger(0);
69     httpTransportGetCount = new AtomicInteger(0);
70     currentTimeInMillis = INITIAL_CURRENT_TIME_IN_MILLIS;
71     executorIsAcceptingRunnables = true;
72     TestKeysDownloader.testInstance = this;
73   }
74 
75   @After
tearDown()76   public void tearDown() throws Exception {
77     executor.shutdownNow();
78     assertTrue(
79         "Timed out while waiting for the threadpool to terminate!",
80         executor.awaitTermination(1, TimeUnit.SECONDS));
81   }
82 
83   @Test
builderShouldThrowIllegalArgumentExceptionWhenUrlIsNotHttps()84   public void builderShouldThrowIllegalArgumentExceptionWhenUrlIsNotHttps() {
85     assertThrows(
86         IllegalArgumentException.class,
87         () -> new KeysDownloader.Builder().setUrl("http://abc").build());
88   }
89 
90   @Test
shouldFetchKeys()91   public void shouldFetchKeys() throws Exception {
92     httpResponseBuilder = new HttpResponseBuilder().setContent("keys");
93 
94     assertEquals("keys", newInstanceForTests().download());
95   }
96 
97   @Test
shouldThrowOnSuccessHttpResponsesThatAreNotOk()98   public void shouldThrowOnSuccessHttpResponsesThatAreNotOk() throws Exception {
99     httpResponseBuilder =
100         new HttpResponseBuilder().setStatusCode(HttpStatusCodes.STATUS_CODE_NO_CONTENT);
101     KeysDownloader instance = newInstanceForTests();
102 
103     IOException expected = assertThrows(IOException.class, instance::download);
104     assertEquals(
105         "Unexpected status code = " + HttpStatusCodes.STATUS_CODE_NO_CONTENT,
106         expected.getMessage());
107   }
108 
109   @Test
shouldThrowOnNonSuccessHttpResponses()110   public void shouldThrowOnNonSuccessHttpResponses() throws Exception {
111     httpResponseBuilder =
112         new HttpResponseBuilder().setStatusCode(HttpStatusCodes.STATUS_CODE_NO_CONTENT);
113     KeysDownloader instance = newInstanceForTests();
114 
115     IOException expected = assertThrows(IOException.class, instance::download);
116     assertTrue(
117         "Message "
118             + expected.getMessage()
119             + " should contain "
120             + HttpStatusCodes.STATUS_CODE_NO_CONTENT,
121         expected.getMessage().contains(Integer.toString(HttpStatusCodes.STATUS_CODE_NO_CONTENT)));
122   }
123 
124   @Test
shouldCacheKeysOnFetches()125   public void shouldCacheKeysOnFetches() throws Exception {
126     KeysDownloader instance = newInstanceForTests();
127     httpResponseBuilder = new HttpResponseBuilder().setContent("keys1");
128     // Fetched and cached keys
129     assertEquals("keys1", instance.download());
130     // Keys changed
131     httpResponseBuilder = new HttpResponseBuilder().setContent("keys2");
132 
133     // Old keys are returned
134     assertEquals("keys1", instance.download());
135   }
136 
137   @Test
shouldFetchKeysAgainIfNoCacheControlHeadersAreSent()138   public void shouldFetchKeysAgainIfNoCacheControlHeadersAreSent() throws Exception {
139     KeysDownloader instance = newInstanceForTests();
140     httpResponseBuilder = new HttpResponseBuilder().setContent("keys1").clearCacheControl();
141     // Fetched and cached keys
142     assertEquals("keys1", instance.download());
143     // Keys changed
144     httpResponseBuilder = new HttpResponseBuilder().setContent("keys2");
145 
146     // New keys are fetched and returned
147     assertEquals("keys2", instance.download());
148   }
149 
150   @Test
shouldFetchKeysAgainAfterExpiration()151   public void shouldFetchKeysAgainAfterExpiration() throws Exception {
152     KeysDownloader instance = newInstanceForTests();
153     httpResponseBuilder =
154         new HttpResponseBuilder().setContent("keys1").setCacheControlWithMaxAgeInSeconds(3L);
155     // Fetched and cached keys
156     assertEquals("keys1", instance.download());
157     // Keys changed
158     httpResponseBuilder = new HttpResponseBuilder().setContent("keys2");
159     // 3 seconds later ...
160     currentTimeInMillis += 3000L;
161 
162     // New keys are fetched and returned
163     assertEquals("keys2", instance.download());
164   }
165 
166   @Test
shouldReturnCachedKeysBeforeExpiration()167   public void shouldReturnCachedKeysBeforeExpiration() throws Exception {
168     KeysDownloader instance = newInstanceForTests();
169     httpResponseBuilder =
170         new HttpResponseBuilder().setContent("keys1").setCacheControlWithMaxAgeInSeconds(3L);
171     // Fetched and cached keys
172     assertEquals("keys1", instance.download());
173     // Keys changed
174     httpResponseBuilder = new HttpResponseBuilder().setContent("keys2");
175     // 3 seconds - 1ms later ...
176     currentTimeInMillis += 3000L - 1;
177 
178     // Old keys are sill returned
179     assertEquals("keys1", instance.download());
180   }
181 
182   @Test
shouldFetchKeysAgainAfterExpirationAccountingForAgeHeader()183   public void shouldFetchKeysAgainAfterExpirationAccountingForAgeHeader() throws Exception {
184     KeysDownloader instance = newInstanceForTests();
185     httpResponseBuilder =
186         new HttpResponseBuilder()
187             .setContent("keys1")
188             .setCacheControlWithMaxAgeInSeconds(3L)
189             .setAgeInSeconds(1L);
190     // Fetched and cached keys
191     assertEquals("keys1", instance.download());
192     // Keys changed
193     httpResponseBuilder = new HttpResponseBuilder().setContent("keys2");
194     // 2 seconds later ...
195     currentTimeInMillis += 2000L;
196 
197     // New keys are fetched and returned
198     assertEquals("keys2", instance.download());
199   }
200 
201   @Test
shouldReturnCachedKeysBeforeExpirationAccountingForAgeHeader()202   public void shouldReturnCachedKeysBeforeExpirationAccountingForAgeHeader() throws Exception {
203     KeysDownloader instance = newInstanceForTests();
204     httpResponseBuilder =
205         new HttpResponseBuilder()
206             .setContent("keys1")
207             .setCacheControlWithMaxAgeInSeconds(3L)
208             .setAgeInSeconds(1L);
209     // Fetched and cached keys
210     assertEquals("keys1", instance.download());
211     // Keys changed
212     httpResponseBuilder = new HttpResponseBuilder().setContent("keys2");
213     // 2 seconds - 1ms later ...
214     currentTimeInMillis += 2000L - 1;
215 
216     // Old keys are sill returned
217     assertEquals("keys1", instance.download());
218   }
219 
220   @Test
shouldTriggerBackgroundRefreshHalfWayThroughExpiration()221   public void shouldTriggerBackgroundRefreshHalfWayThroughExpiration() throws Exception {
222     KeysDownloader instance = newInstanceForTests();
223     httpResponseBuilder =
224         new HttpResponseBuilder().setContent("keys1").setCacheControlWithMaxAgeInSeconds(3L);
225     // Fetched and cached keys
226     assertEquals("keys1", instance.download());
227     // Keys changed
228     httpResponseBuilder = new HttpResponseBuilder().setContent("keys2");
229     // 1.5 seconds later ...
230     currentTimeInMillis += 1500L;
231     // Old keys are sill returned, but a background fetch is initiated
232     assertEquals("keys1", instance.download());
233     // Wait background fetch to complete
234     waitForLatch(backgroundFetchFinishedLatch);
235     // 10ms later ...
236     currentTimeInMillis += 10;
237     // Keys changed again
238     httpResponseBuilder = new HttpResponseBuilder().setContent("keys3");
239 
240     // Keys fetched in the background are used
241     assertEquals("keys2", instance.download());
242     // Single background fetch should have been triggered
243     assertEquals(1, backgroundFetchStartedCount.get());
244   }
245 
246   @Test
shouldNotTriggerBackgroundRefreshBeforeHalfWayThroughExpiration()247   public void shouldNotTriggerBackgroundRefreshBeforeHalfWayThroughExpiration() throws Exception {
248     KeysDownloader instance = newInstanceForTests();
249     httpResponseBuilder =
250         new HttpResponseBuilder().setContent("keys1").setCacheControlWithMaxAgeInSeconds(3L);
251     // Fetched and cached keys
252     assertEquals("keys1", instance.download());
253     // Keys changed
254     httpResponseBuilder = new HttpResponseBuilder().setContent("keys2");
255     // 1.5 seconds - 1ms later ...
256     currentTimeInMillis += 1500L - 1;
257 
258     // Old keys are sill returned
259     assertEquals("keys1", instance.download());
260     // No background fetch should have been triggered
261     assertEquals(0, backgroundFetchStartedCount.get());
262   }
263 
264   @Test
shouldPerformBackgroundRefreshWhenRequestedAndHaveCacheKeys()265   public void shouldPerformBackgroundRefreshWhenRequestedAndHaveCacheKeys() throws Exception {
266     KeysDownloader instance = newInstanceForTests();
267     httpResponseBuilder =
268         new HttpResponseBuilder().setContent("keys1").setCacheControlWithMaxAgeInSeconds(3L);
269     // Fetched and cache keys
270     instance.refreshInBackground();
271     // Wait background fetch to complete
272     waitForLatch(backgroundFetchFinishedLatch);
273     // Keys changed
274     httpResponseBuilder = new HttpResponseBuilder().setContent("keys2");
275 
276     // Keys fetched in the background are used
277     assertEquals("keys1", instance.download());
278     // Single background fetch should have been triggered
279     assertEquals(1, backgroundFetchStartedCount.get());
280     // Single http fetch should have been triggered
281     assertEquals(1, httpTransportGetCount.get());
282   }
283 
284   @Test
shouldPerformMultipleRefreshesWhenRequested()285   public void shouldPerformMultipleRefreshesWhenRequested() throws Exception {
286     KeysDownloader instance = newInstanceForTests();
287     httpResponseBuilder = new HttpResponseBuilder().setContent("keys1");
288     instance.refreshInBackground();
289     waitForLatch(backgroundFetchFinishedLatch);
290     httpResponseBuilder = new HttpResponseBuilder().setContent("keys2");
291     backgroundFetchFinishedLatch = new CountDownLatch(1);
292     instance.refreshInBackground();
293     waitForLatch(backgroundFetchFinishedLatch);
294 
295     // Keys fetched in the background are used
296     assertEquals("keys2", instance.download());
297     // Multiple background fetch should have been triggered
298     assertEquals(2, backgroundFetchStartedCount.get());
299     // Multiple http fetch should have been triggered
300     assertEquals(2, httpTransportGetCount.get());
301   }
302 
303   @Test
shouldPerformRefreshAfterExecutorTransientFailure()304   public void shouldPerformRefreshAfterExecutorTransientFailure() throws Exception {
305     KeysDownloader instance = newInstanceForTests();
306     httpResponseBuilder = new HttpResponseBuilder().setContent("keys1");
307     Object unused = instance.download();
308     httpResponseBuilder = new HttpResponseBuilder().setContent("keys2");
309     // Executor temporarily full, rejecting new Runnable instances
310     executorIsAcceptingRunnables = false;
311     assertThrows(RejectedExecutionException.class, instance::refreshInBackground);
312     httpResponseBuilder = new HttpResponseBuilder().setContent("keys3");
313     // Executor available again, accepting new Runnable instances
314     executorIsAcceptingRunnables = true;
315     instance.refreshInBackground();
316     waitForLatch(backgroundFetchFinishedLatch);
317 
318     // Keys fetched in the background are used
319     assertEquals("keys3", instance.download());
320     // Only a single background fetch should have started
321     assertEquals(1, backgroundFetchStartedCount.get());
322   }
323 
324   @Test
shouldFetchOnlyOnceWhenMultipleThreadsTryToGetKeys()325   public void shouldFetchOnlyOnceWhenMultipleThreadsTryToGetKeys() throws Exception {
326     final KeysDownloader instance = newInstanceForTests();
327     httpResponseBuilder = new HttpResponseBuilder().setContent("keys");
328     List<FutureTask<String>> tasks = new ArrayList<>();
329     for (int i = 0; i < 10; i++) {
330       tasks.add(
331           new FutureTask<String>(
332               new Callable<String>() {
333                 @Override
334                 public String call() throws Exception {
335                   return instance.download();
336                 }
337               }));
338     }
339 
340     // Force the HTTP responses to be delayed until the latch goes down to 0.
341     delayHttpResponseLatch = new CountDownLatch(1);
342     // Execute the all fetches in parallel.
343     for (FutureTask<String> task : tasks) {
344       executor.execute(task);
345     }
346     // Releasing the response.
347     delayHttpResponseLatch.countDown();
348 
349     for (FutureTask<String> task : tasks) {
350       assertEquals("keys", task.get(5, TimeUnit.SECONDS));
351     }
352     // Should only have hit the network once.
353     assertEquals(1, httpTransportGetCount.get());
354   }
355 
356   @Test
357   public void
shouldFetchOnlyOnceInBackgroundHalfWayThroughExpirationWhenMultipleThreadsTryToGetKeys()358       shouldFetchOnlyOnceInBackgroundHalfWayThroughExpirationWhenMultipleThreadsTryToGetKeys()
359           throws Exception {
360     final KeysDownloader instance = newInstanceForTests();
361     httpResponseBuilder =
362         new HttpResponseBuilder().setContent("keys1").setCacheControlWithMaxAgeInSeconds(3L);
363     // Fetched and cached keys
364     assertEquals("keys1", instance.download());
365     // Keys changed
366     httpResponseBuilder = new HttpResponseBuilder().setContent("keys2");
367     // 1.5 seconds later
368     currentTimeInMillis += 1500L;
369     List<FutureTask<String>> tasks = new ArrayList<>();
370     for (int i = 0; i < 10; i++) {
371       tasks.add(
372           new FutureTask<String>(
373               new Callable<String>() {
374                 @Override
375                 public String call() throws Exception {
376                   return instance.download();
377                 }
378               }));
379     }
380     // Resetting counters
381     httpTransportGetCount.set(0);
382     backgroundFetchStartedCount.set(0);
383 
384     // Force the HTTP responses to be delayed until the latch goes down to 0.
385     delayHttpResponseLatch = new CountDownLatch(1);
386     // Execute the all fetches in parallel.
387     for (FutureTask<String> task : tasks) {
388       executor.execute(task);
389     }
390     // Wait for all of them to complete (will use old keys that were cached)
391     for (FutureTask<String> task : tasks) {
392       assertEquals("keys1", task.get(5, TimeUnit.SECONDS));
393     }
394     // Releasing the response.
395     delayHttpResponseLatch.countDown();
396     // Waiting background fetch to finish
397     waitForLatch(backgroundFetchFinishedLatch);
398 
399     // Only a single background fetch should have been triggered
400     assertEquals(1, backgroundFetchStartedCount.get());
401     // Should only have hit the network once.
402     assertEquals(1, httpTransportGetCount.get());
403   }
404 
waitForLatch(CountDownLatch latch)405   private static void waitForLatch(CountDownLatch latch) {
406     try {
407       assertTrue("Timed out!", latch.await(5, TimeUnit.SECONDS));
408     } catch (InterruptedException e) {
409       throw new RuntimeException(e);
410     }
411   }
412 
newInstanceForTests()413   private TestKeysDownloader newInstanceForTests() {
414     return new TestKeysDownloader(
415         new Executor() {
416           @Override
417           public void execute(final Runnable command) {
418             if (!executorIsAcceptingRunnables) {
419               throw new RejectedExecutionException();
420             }
421             executor.execute(
422                 new Runnable() {
423                   @Override
424                   public void run() {
425                     backgroundFetchStartedCount.incrementAndGet();
426                     try {
427                       command.run();
428                     } finally {
429                       backgroundFetchFinishedLatch.countDown();
430                     }
431                   }
432                 });
433           }
434         },
435         new MockHttpTransport() {
436           @Override
437           public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
438             httpTransportGetCount.incrementAndGet();
439             if (delayHttpResponseLatch != null) {
440               waitForLatch(delayHttpResponseLatch);
441             }
442             assertEquals("https://someUrl", url);
443             assertEquals("GET", method);
444             MockLowLevelHttpRequest request = new MockLowLevelHttpRequest(url);
445             request.setResponse(httpResponseBuilder.build());
446             return request;
447           }
448         },
449         "https://someUrl");
450   }
451 
452   private static class TestKeysDownloader extends KeysDownloader {
453     private static KeysDownloaderTest testInstance;
454 
455     TestKeysDownloader(Executor backgroundExecutor, HttpTransport httpTransport, String keysUrl) {
456       super(backgroundExecutor, httpTransport, keysUrl);
457     }
458 
459     @Override
460     long getCurrentTimeInMillis() {
461       return testInstance.currentTimeInMillis;
462     }
463   }
464 
465   private static class HttpResponseBuilder {
466     private String content = "content";
467     private Long maxAgeInSeconds = 10L;
468     private Long ageInSeconds;
469     private int statusCode = HttpStatusCodes.STATUS_CODE_OK;
470 
471     @CanIgnoreReturnValue
472     public HttpResponseBuilder setStatusCode(int statusCode) {
473       this.statusCode = statusCode;
474       return this;
475     }
476 
477     @CanIgnoreReturnValue
478     public HttpResponseBuilder setContent(String content) {
479       this.content = content;
480       return this;
481     }
482 
483     @CanIgnoreReturnValue
484     public HttpResponseBuilder setCacheControlWithMaxAgeInSeconds(Long maxAgeInSeconds) {
485       this.maxAgeInSeconds = maxAgeInSeconds;
486       return this;
487     }
488 
489     @CanIgnoreReturnValue
490     public HttpResponseBuilder clearCacheControl() {
491       this.maxAgeInSeconds = null;
492       return this;
493     }
494 
495     @CanIgnoreReturnValue
496     public HttpResponseBuilder setAgeInSeconds(Long ageInSeconds) {
497       this.ageInSeconds = ageInSeconds;
498       return this;
499     }
500 
501     public MockLowLevelHttpResponse build() {
502       MockLowLevelHttpResponse response =
503           new MockLowLevelHttpResponse().setContent(content).setStatusCode(statusCode);
504       if (ageInSeconds != null) {
505         response.addHeader("Age", Long.toString(ageInSeconds));
506       }
507       if (maxAgeInSeconds != null) {
508         response.addHeader("Cache-Control", "public, max-age=" + maxAgeInSeconds);
509       }
510       return response;
511     }
512   }
513 }
514