/* * Copyright 2020 Slawomir Jaranowski * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.simplify4u.plugins.keyserver; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions; import com.google.common.io.ByteStreams; import com.google.common.io.MoreFiles; import com.google.common.io.RecursiveDeleteOption; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.simplify4u.plugins.keyserver.PGPKeysCache.KeyServerList; import org.simplify4u.plugins.keyserver.PGPKeysCache.KeyServerListFallback; import org.simplify4u.plugins.keyserver.PGPKeysCache.KeyServerListLoadBalance; import org.simplify4u.plugins.keyserver.PGPKeysCache.KeyServerListOne; import org.simplify4u.plugins.utils.PGPKeyId; import org.simplify4u.plugins.utils.PGPKeyId.PGPKeyIdLong; import org.simplify4u.sjf4jmock.LoggerMock; import org.slf4j.Logger; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; public class PGPKeysCacheTest { public static final PGPKeyId KEY_ID_1 = PGPKeyId.from(1L); private Path cachePath; private List<PGPKeysServerClient> keysServerClients; @BeforeMethod public void setup() throws IOException { LoggerMock.clearInvocations(); cachePath = Files.createTempDirectory("cache-path-test"); PGPKeysServerClient keysServerClient = mock(PGPKeysServerClient.class); doAnswer(i -> new URI(String.format("https://key.get.example.com/?keyId=%s", (PGPKeyId)i.getArgument(0)))) .when(keysServerClient).getUriForGetKey(any(PGPKeyId.class)); doAnswer(i -> { try (InputStream inputStream = getClass().getResourceAsStream("/EFE8086F9E93774E.asc")) { ByteStreams.copy(inputStream, i.getArgument(1)); } return null; }).when(keysServerClient).copyKeyToOutputStream(any(PGPKeyId.class), any(OutputStream.class), any(PGPKeysServerClient.OnRetryConsumer.class)); keysServerClients = Collections.singletonList(keysServerClient); } @AfterMethod public void cleanup() throws IOException { MoreFiles.deleteRecursively(cachePath, RecursiveDeleteOption.ALLOW_INSECURE); } @Test public void emptyCacheDirShouldBeCreated() throws IOException { File emptyCachePath = new File(cachePath.toFile(), "empty"); assertThat(emptyCachePath).doesNotExist(); new PGPKeysCache(emptyCachePath, keysServerClients, true); assertThat(emptyCachePath) .exists() .isDirectory(); } @Test public void fileAsCacheDirThrowException() throws IOException { File fileAsCachePath = new File(cachePath.toFile(), "file.tmp"); MoreFiles.touch(fileAsCachePath.toPath()); assertThat(fileAsCachePath) .exists() .isFile(); assertThatCode(() -> new PGPKeysCache(fileAsCachePath, keysServerClients, true)) .isExactlyInstanceOf(IOException.class) .hasMessageStartingWith("PGP keys cache path exist but is not a directory:"); } @Test public void getKeyFromCache() throws IOException, PGPException { PGPKeysCache pgpKeysCache = new PGPKeysCache(cachePath.toFile(), keysServerClients, true); // first call retrieve key from server PGPPublicKeyRing keyRing = pgpKeysCache.getKeyRing(PGPKeyId.from(0xEFE8086F9E93774EL)); assertThat(keyRing) .hasSize(2) .anyMatch(key -> key.getKeyID() == 0xEFE8086F9E93774EL); verify(keysServerClients.get(0)).getUriForGetKey(any(PGPKeyId.class)); verify(keysServerClients.get(0)).copyKeyToOutputStream(any(PGPKeyIdLong.class), any(OutputStream.class), any(PGPKeysServerClient.OnRetryConsumer.class)); verifyNoMoreInteractions(keysServerClients.get(0)); clearInvocations(keysServerClients.get(0)); // second from cache keyRing = pgpKeysCache.getKeyRing(PGPKeyId.from(0xEFE8086F9E93774EL)); assertThat(keyRing) .hasSize(2) .anyMatch(key -> key.getKeyID() == 0xEFE8086F9E93774EL); verifyNoInteractions(keysServerClients.get(0)); } @Test public void nonExistingKeyInRingThrowException() throws IOException, PGPException { PGPKeysCache pgpKeysCache = new PGPKeysCache(cachePath.toFile(), keysServerClients, true); // first call retrieve key from server assertThatCode(() -> pgpKeysCache.getKeyRing(PGPKeyId.from(0x1234567890L))) .isExactlyInstanceOf(PGPException.class) .hasMessageStartingWith("Can't find public key 0x0000001234567890 in download file:"); } @DataProvider(name = "serverListTestData") public Object[][] serverListTestData() { PGPKeysServerClient client1 = mock(PGPKeysServerClient.class); PGPKeysServerClient client2 = mock(PGPKeysServerClient.class); return new Object[][]{ {Collections.singletonList(client1), true, KeyServerListOne.class}, {Collections.singletonList(client1), false, KeyServerListOne.class}, {Arrays.asList(client1, client2), true, KeyServerListLoadBalance.class}, {Arrays.asList(client1, client2), false, KeyServerListFallback.class} }; } @Test(dataProvider = "serverListTestData") public void createKeyServerListReturnCorrectImplementation( List<PGPKeysServerClient> serverList, boolean loadBalance, Class<? extends KeyServerList> aClass) { KeyServerList keyServerList = PGPKeysCache.createKeyServerList(serverList, loadBalance); assertThat(keyServerList).isExactlyInstanceOf(aClass); } @Test public void listOneUseFirstServerForCorrectExecute() throws IOException { PGPKeysServerClient client1 = mock(PGPKeysServerClient.class); PGPKeysServerClient client2 = mock(PGPKeysServerClient.class); List<PGPKeysServerClient> executedClient = new ArrayList<>(); KeyServerList serverList = new KeyServerListOne().withClients(Arrays.asList(client1, client2)); for (int i = 0; i < 2; i++) { serverList.execute(client -> { client.copyKeyToOutputStream(KEY_ID_1, null, null); executedClient.add(client); }); serverList.getUriForShowKey(KEY_ID_1); } assertThat(executedClient).containsOnly(client1, client1); verify(client1, times(2)).copyKeyToOutputStream(KEY_ID_1, null, null); verify(client1, times(2)).getUriForShowKey(KEY_ID_1); verifyNoMoreInteractions(client1); verifyNoInteractions(client2); } @Test public void listOneThrowsExceptionForFailedExecute() throws IOException { PGPKeysServerClient client1 = mock(PGPKeysServerClient.class); PGPKeysServerClient client2 = mock(PGPKeysServerClient.class); doThrow(new IOException("Fallback test")).when(client1).copyKeyToOutputStream(KEY_ID_1, null, null); KeyServerList serverListFallback = new KeyServerListOne().withClients(Arrays.asList(client1, client2)); assertThatCode(() -> serverListFallback.execute(client -> client.copyKeyToOutputStream(KEY_ID_1, null, null))) .isExactlyInstanceOf(IOException.class) .hasMessage("Fallback test"); verify(client1).copyKeyToOutputStream(KEY_ID_1, null, null); verifyNoMoreInteractions(client1); verifyNoInteractions(client2); } @Test public void fallbackOnlyUseFirstServerForCorrectExecute() throws IOException { PGPKeysServerClient client1 = mock(PGPKeysServerClient.class); PGPKeysServerClient client2 = mock(PGPKeysServerClient.class); List<PGPKeysServerClient> executedClient = new ArrayList<>(); KeyServerList serverListFallback = new KeyServerListFallback().withClients(Arrays.asList(client1, client2)); for (int i = 0; i < 2; i++) { serverListFallback.execute(client -> { client.copyKeyToOutputStream(KEY_ID_1, null, null); executedClient.add(client); }); serverListFallback.getUriForShowKey(KEY_ID_1); } assertThat(executedClient).containsOnly(client1, client1); verify(client1, times(2)).copyKeyToOutputStream(KEY_ID_1, null, null); verify(client1, times(2)).getUriForShowKey(KEY_ID_1); verifyNoMoreInteractions(client1); verifyNoInteractions(client2); } @Test public void loadBalanceIterateByAllServer() throws IOException { PGPKeysServerClient client1 = mock(PGPKeysServerClient.class); PGPKeysServerClient client2 = mock(PGPKeysServerClient.class); List<PGPKeysServerClient> executedClient = new ArrayList<>(); KeyServerList serverListFallback = new KeyServerListLoadBalance().withClients(Arrays.asList(client1, client2)); for (int i = 0; i < 3; i++) { serverListFallback.execute(client -> { client.copyKeyToOutputStream(KEY_ID_1, null, null); executedClient.add(client); }); serverListFallback.getUriForShowKey(KEY_ID_1); } assertThat(executedClient).containsExactly(client1, client2, client1); verify(client1, times(2)).copyKeyToOutputStream(KEY_ID_1, null, null); verify(client1, times(2)).getUriForShowKey(KEY_ID_1); verifyNoMoreInteractions(client1); verify(client2).copyKeyToOutputStream(KEY_ID_1, null, null); verify(client2).getUriForShowKey(KEY_ID_1); verifyNoMoreInteractions(client1); } @DataProvider(name = "keyServerListWithFallBack") public Object[] keyServerListWithFallBack() { return new Object[]{ new KeyServerListFallback(), new KeyServerListLoadBalance() }; } @Test(dataProvider = "keyServerListWithFallBack") public void useSecondServerForFailedExecute(KeyServerList keyServerList) throws IOException { PGPKeysServerClient client1 = mock(PGPKeysServerClient.class); PGPKeysServerClient client2 = mock(PGPKeysServerClient.class); doThrow(new IOException("Fallback test")).when(client1).copyKeyToOutputStream(KEY_ID_1, null, null); keyServerList.withClients(Arrays.asList(client1, client2)); List<PGPKeysServerClient> executedClient = new ArrayList<>(); for (int i = 0; i < 2; i++) { keyServerList.execute(client -> { client.copyKeyToOutputStream(KEY_ID_1, null, null); executedClient.add(client); }); keyServerList.getUriForShowKey(KEY_ID_1); } assertThat(executedClient).containsExactly(client2, client2); verify(client1, times(2)).copyKeyToOutputStream(KEY_ID_1, null, null); verifyNoMoreInteractions(client1); verify(client2, times(2)).copyKeyToOutputStream(KEY_ID_1, null, null); verify(client2, times(2)).getUriForShowKey(KEY_ID_1); verifyNoMoreInteractions(client2); } @Test(dataProvider = "keyServerListWithFallBack") public void throwsExceptionForAllFailedExecute(KeyServerList keyServerList) throws IOException { PGPKeysServerClient client1 = mock(PGPKeysServerClient.class); PGPKeysServerClient client2 = mock(PGPKeysServerClient.class); doThrow(new IOException("Fallback test1")).when(client1).copyKeyToOutputStream(KEY_ID_1, null, null); doThrow(new IOException("Fallback test2")).when(client2).copyKeyToOutputStream(KEY_ID_1, null, null); keyServerList.withClients(Arrays.asList(client1, client2)); assertThatCode(() -> keyServerList.execute(client -> client.copyKeyToOutputStream(KEY_ID_1, null, null))) .isExactlyInstanceOf(IOException.class) .hasMessage("Fallback test2"); Logger keysCacheLogger = LoggerMock.getLoggerMock(PGPKeysCache.class); verify(keysCacheLogger).warn(eq("{} throw exception: {} - {} try next client"), eq(client1), eq("Fallback test1"), anyString()); verify(keysCacheLogger).warn(eq("{} throw exception: {} - {} try next client"), eq(client2), eq("Fallback test2"), anyString()); verify(keysCacheLogger).error("All servers from list was failed"); verifyNoMoreInteractions(keysCacheLogger); verify(client1).copyKeyToOutputStream(KEY_ID_1, null, null); verifyNoMoreInteractions(client1); verify(client2).copyKeyToOutputStream(KEY_ID_1, null, null); verifyNoMoreInteractions(client2); } @Test(dataProvider = "keyServerListWithFallBack") public void throwsPGPKeyNotFoundWhenKeyNotFoundOnAnyServer(KeyServerList keyServerList) throws IOException { PGPKeysServerClient client1 = mock(PGPKeysServerClient.class); PGPKeysServerClient client2 = mock(PGPKeysServerClient.class); doThrow(new PGPKeyNotFound()).when(client1).copyKeyToOutputStream(KEY_ID_1, null, null); doThrow(new PGPKeyNotFound()).when(client2).copyKeyToOutputStream(KEY_ID_1, null, null); keyServerList.withClients(Arrays.asList(client1, client2)); assertThatCode(() -> keyServerList.execute(client -> client.copyKeyToOutputStream(KEY_ID_1, null, null))) .isExactlyInstanceOf(PGPKeyNotFound.class); Logger keysCacheLogger = LoggerMock.getLoggerMock(PGPKeysCache.class); verify(keysCacheLogger).warn(eq("{} throw exception: {} - {} try next client"), eq(client1), isNull(), anyString()); verify(keysCacheLogger).warn(eq("{} throw exception: {} - {} try next client"), eq(client2), isNull(), anyString()); verify(keysCacheLogger).error("All servers from list was failed"); verifyNoMoreInteractions(keysCacheLogger); verify(client1).copyKeyToOutputStream(KEY_ID_1, null, null); verifyNoMoreInteractions(client1); verify(client2).copyKeyToOutputStream(KEY_ID_1, null, null); verifyNoMoreInteractions(client2); } }