Refactoring
[jFCPlib.git] / src / test / java / net / pterodactylus / fcp / quelaton / DefaultFcpClientTest.java
index 9eef9ce..dae4111 100644 (file)
@@ -1,11 +1,15 @@
 package net.pterodactylus.fcp.quelaton;
 
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.allOf;
 import static org.hamcrest.Matchers.contains;
 import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.hasItem;
 import static org.hamcrest.Matchers.hasSize;
 import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
 import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.startsWith;
 
 import java.io.ByteArrayInputStream;
 import java.io.File;
@@ -16,13 +20,18 @@ import java.util.Collection;
 import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
 import net.pterodactylus.fcp.ARK;
+import net.pterodactylus.fcp.ConfigData;
 import net.pterodactylus.fcp.DSAGroup;
 import net.pterodactylus.fcp.FcpKeyPair;
 import net.pterodactylus.fcp.Key;
@@ -30,24 +39,28 @@ import net.pterodactylus.fcp.NodeData;
 import net.pterodactylus.fcp.NodeRef;
 import net.pterodactylus.fcp.Peer;
 import net.pterodactylus.fcp.PeerNote;
+import net.pterodactylus.fcp.PluginInfo;
 import net.pterodactylus.fcp.Priority;
 import net.pterodactylus.fcp.fake.FakeTcpServer;
 import net.pterodactylus.fcp.quelaton.ClientGetCommand.Data;
 
 import com.google.common.io.ByteStreams;
 import com.google.common.io.Files;
+import com.nitorcreations.junit.runners.NestedRunner;
 import org.hamcrest.Description;
 import org.hamcrest.Matcher;
 import org.hamcrest.TypeSafeDiagnosingMatcher;
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 
 /**
  * Unit test for {@link DefaultFcpClient}.
  *
  * @author <a href="bombe@freenetproject.org">David ‘Bombe’ Roden</a>
  */
+@RunWith(NestedRunner.class)
 public class DefaultFcpClientTest {
 
        private static final String INSERT_URI =
@@ -55,13 +68,13 @@ public class DefaultFcpClientTest {
        private static final String REQUEST_URI =
                "SSK@wtbgd2loNcJCXvtQVOftl2tuWBomDQHfqS6ytpPRhfw,7SHH53gletBVb9JD7nBsyClbLQsBubDPEIcwg908r7Y,AQACAAE/";
 
-       private static int threadCounter = 0;
+       private int threadCounter = 0;
+       private final ExecutorService threadPool =
+               Executors.newCachedThreadPool(r -> new Thread(r, "Test-Thread-" + threadCounter++));
        private final FakeTcpServer fcpServer;
        private final DefaultFcpClient fcpClient;
 
        public DefaultFcpClientTest() throws IOException {
-               ExecutorService threadPool =
-                       Executors.newCachedThreadPool(r -> new Thread(r, "Test-Thread-" + threadCounter++));
                fcpServer = new FakeTcpServer(threadPool);
                fcpClient = new DefaultFcpClient(threadPool, "localhost", fcpServer.getPort(), () -> "Test");
        }
@@ -69,6 +82,7 @@ public class DefaultFcpClientTest {
        @After
        public void tearDown() throws IOException {
                fcpServer.close();
+               threadPool.shutdown();
        }
 
        @Test(expected = ExecutionException.class)
@@ -1242,4 +1256,1081 @@ public class DefaultFcpClientTest {
                assertThat(peerNote.get().get().getPeerNoteType(), is(1));
        }
 
+       @Test
+       public void defaultFcpClientCanEnablePeerByName() throws InterruptedException, ExecutionException, IOException {
+               Future<Optional<Peer>> peer = fcpClient.modifyPeer().enable().byName("Friend1").execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "ModifyPeer",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=Friend1",
+                       "IsDisabled=false",
+                       "EndMessage"
+               ));
+               fcpServer.writeLine(
+                       "Peer",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=Friend1",
+                       "identity=id1",
+                       "EndMessage"
+               );
+               assertThat(peer.get().get().getIdentity(), is("id1"));
+       }
+
+       @Test
+       public void defaultFcpClientCanDisablePeerByName() throws InterruptedException, ExecutionException, IOException {
+               Future<Optional<Peer>> peer = fcpClient.modifyPeer().disable().byName("Friend1").execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "ModifyPeer",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=Friend1",
+                       "IsDisabled=true",
+                       "EndMessage"
+               ));
+               fcpServer.writeLine(
+                       "Peer",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=Friend1",
+                       "identity=id1",
+                       "EndMessage"
+               );
+               assertThat(peer.get().get().getIdentity(), is("id1"));
+       }
+
+       @Test
+       public void defaultFcpClientCanEnablePeerByIdentity() throws InterruptedException, ExecutionException, IOException {
+               Future<Optional<Peer>> peer = fcpClient.modifyPeer().enable().byIdentity("id1").execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "ModifyPeer",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=id1",
+                       "IsDisabled=false",
+                       "EndMessage"
+               ));
+               fcpServer.writeLine(
+                       "Peer",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=Friend1",
+                       "identity=id1",
+                       "EndMessage"
+               );
+               assertThat(peer.get().get().getIdentity(), is("id1"));
+       }
+
+       @Test
+       public void defaultFcpClientCanEnablePeerByHostAndPort()
+       throws InterruptedException, ExecutionException, IOException {
+               Future<Optional<Peer>> peer = fcpClient.modifyPeer().enable().byHostAndPort("1.2.3.4", 5678).execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "ModifyPeer",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=1.2.3.4:5678",
+                       "IsDisabled=false",
+                       "EndMessage"
+               ));
+               fcpServer.writeLine(
+                       "Peer",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=Friend1",
+                       "identity=id1",
+                       "EndMessage"
+               );
+               assertThat(peer.get().get().getIdentity(), is("id1"));
+       }
+
+       @Test
+       public void defaultFcpClientCanNotModifyPeerOfUnknownNode()
+       throws InterruptedException, ExecutionException, IOException {
+               Future<Optional<Peer>> peer = fcpClient.modifyPeer().enable().byIdentity("id1").execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "ModifyPeer",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=id1",
+                       "IsDisabled=false",
+                       "EndMessage"
+               ));
+               fcpServer.writeLine(
+                       "UnknownNodeIdentifier",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=id1",
+                       "EndMessage"
+               );
+               assertThat(peer.get().isPresent(), is(false));
+       }
+
+       @Test
+       public void defaultFcpClientCanAllowLocalAddressesOfPeer()
+       throws InterruptedException, ExecutionException, IOException {
+               Future<Optional<Peer>> peer = fcpClient.modifyPeer().allowLocalAddresses().byIdentity("id1").execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "ModifyPeer",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=id1",
+                       "AllowLocalAddresses=true",
+                       "EndMessage"
+               ));
+               assertThat(lines, not(contains(startsWith("IsDisabled="))));
+               fcpServer.writeLine(
+                       "Peer",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=Friend1",
+                       "identity=id1",
+                       "EndMessage"
+               );
+               assertThat(peer.get().get().getIdentity(), is("id1"));
+       }
+
+       @Test
+       public void defaultFcpClientCanDisallowLocalAddressesOfPeer()
+       throws InterruptedException, ExecutionException, IOException {
+               Future<Optional<Peer>> peer = fcpClient.modifyPeer().disallowLocalAddresses().byIdentity("id1").execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "ModifyPeer",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=id1",
+                       "AllowLocalAddresses=false",
+                       "EndMessage"
+               ));
+               assertThat(lines, not(contains(startsWith("IsDisabled="))));
+               fcpServer.writeLine(
+                       "Peer",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=Friend1",
+                       "identity=id1",
+                       "EndMessage"
+               );
+               assertThat(peer.get().get().getIdentity(), is("id1"));
+       }
+
+       @Test
+       public void defaultFcpClientCanSetBurstOnlyForPeer()
+       throws InterruptedException, ExecutionException, IOException {
+               Future<Optional<Peer>> peer = fcpClient.modifyPeer().setBurstOnly().byIdentity("id1").execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "ModifyPeer",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=id1",
+                       "IsBurstOnly=true",
+                       "EndMessage"
+               ));
+               assertThat(lines, not(contains(startsWith("AllowLocalAddresses="))));
+               assertThat(lines, not(contains(startsWith("IsDisabled="))));
+               fcpServer.writeLine(
+                       "Peer",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=Friend1",
+                       "identity=id1",
+                       "EndMessage"
+               );
+               assertThat(peer.get().get().getIdentity(), is("id1"));
+       }
+
+       @Test
+       public void defaultFcpClientCanClearBurstOnlyForPeer()
+       throws InterruptedException, ExecutionException, IOException {
+               Future<Optional<Peer>> peer = fcpClient.modifyPeer().clearBurstOnly().byIdentity("id1").execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "ModifyPeer",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=id1",
+                       "IsBurstOnly=false",
+                       "EndMessage"
+               ));
+               assertThat(lines, not(contains(startsWith("AllowLocalAddresses="))));
+               assertThat(lines, not(contains(startsWith("IsDisabled="))));
+               fcpServer.writeLine(
+                       "Peer",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=Friend1",
+                       "identity=id1",
+                       "EndMessage"
+               );
+               assertThat(peer.get().get().getIdentity(), is("id1"));
+       }
+
+       @Test
+       public void defaultFcpClientCanSetListenOnlyForPeer()
+       throws InterruptedException, ExecutionException, IOException {
+               Future<Optional<Peer>> peer = fcpClient.modifyPeer().setListenOnly().byIdentity("id1").execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "ModifyPeer",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=id1",
+                       "IsListenOnly=true",
+                       "EndMessage"
+               ));
+               assertThat(lines, not(contains(startsWith("AllowLocalAddresses="))));
+               assertThat(lines, not(contains(startsWith("IsDisabled="))));
+               assertThat(lines, not(contains(startsWith("IsBurstOnly="))));
+               fcpServer.writeLine(
+                       "Peer",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=Friend1",
+                       "identity=id1",
+                       "EndMessage"
+               );
+               assertThat(peer.get().get().getIdentity(), is("id1"));
+       }
+
+       @Test
+       public void defaultFcpClientCanClearListenOnlyForPeer()
+       throws InterruptedException, ExecutionException, IOException {
+               Future<Optional<Peer>> peer = fcpClient.modifyPeer().clearListenOnly().byIdentity("id1").execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "ModifyPeer",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=id1",
+                       "IsListenOnly=false",
+                       "EndMessage"
+               ));
+               assertThat(lines, not(contains(startsWith("AllowLocalAddresses="))));
+               assertThat(lines, not(contains(startsWith("IsDisabled="))));
+               assertThat(lines, not(contains(startsWith("IsBurstOnly="))));
+               fcpServer.writeLine(
+                       "Peer",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=Friend1",
+                       "identity=id1",
+                       "EndMessage"
+               );
+               assertThat(peer.get().get().getIdentity(), is("id1"));
+       }
+
+       @Test
+       public void defaultFcpClientCanIgnoreSourceForPeer()
+       throws InterruptedException, ExecutionException, IOException {
+               Future<Optional<Peer>> peer = fcpClient.modifyPeer().ignoreSource().byIdentity("id1").execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "ModifyPeer",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=id1",
+                       "IgnoreSourcePort=true",
+                       "EndMessage"
+               ));
+               assertThat(lines, not(contains(startsWith("AllowLocalAddresses="))));
+               assertThat(lines, not(contains(startsWith("IsDisabled="))));
+               assertThat(lines, not(contains(startsWith("IsBurstOnly="))));
+               assertThat(lines, not(contains(startsWith("IsListenOnly="))));
+               fcpServer.writeLine(
+                       "Peer",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=Friend1",
+                       "identity=id1",
+                       "EndMessage"
+               );
+               assertThat(peer.get().get().getIdentity(), is("id1"));
+       }
+
+       @Test
+       public void defaultFcpClientCanUseSourceForPeer()
+       throws InterruptedException, ExecutionException, IOException {
+               Future<Optional<Peer>> peer = fcpClient.modifyPeer().useSource().byIdentity("id1").execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "ModifyPeer",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=id1",
+                       "IgnoreSourcePort=false",
+                       "EndMessage"
+               ));
+               assertThat(lines, not(contains(startsWith("AllowLocalAddresses="))));
+               assertThat(lines, not(contains(startsWith("IsDisabled="))));
+               assertThat(lines, not(contains(startsWith("IsBurstOnly="))));
+               assertThat(lines, not(contains(startsWith("IsListenOnly="))));
+               fcpServer.writeLine(
+                       "Peer",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=Friend1",
+                       "identity=id1",
+                       "EndMessage"
+               );
+               assertThat(peer.get().get().getIdentity(), is("id1"));
+       }
+
+       @Test
+       public void defaultFcpClientCanRemovePeerByName() throws InterruptedException, ExecutionException, IOException {
+               Future<Boolean> peer = fcpClient.removePeer().byName("Friend1").execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "RemovePeer",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=Friend1",
+                       "EndMessage"
+               ));
+               fcpServer.writeLine(
+                       "PeerRemoved",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=Friend1",
+                       "EndMessage"
+               );
+               assertThat(peer.get(), is(true));
+       }
+
+       @Test
+       public void defaultFcpClientCanNotRemovePeerByInvalidName()
+       throws InterruptedException, ExecutionException, IOException {
+               Future<Boolean> peer = fcpClient.removePeer().byName("NotFriend1").execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "RemovePeer",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=NotFriend1",
+                       "EndMessage"
+               ));
+               fcpServer.writeLine(
+                       "UnknownNodeIdentifier",
+                       "Identifier=" + identifier,
+                       "EndMessage"
+               );
+               assertThat(peer.get(), is(false));
+       }
+
+       @Test
+       public void defaultFcpClientCanRemovePeerByIdentity() throws InterruptedException, ExecutionException, IOException {
+               Future<Boolean> peer = fcpClient.removePeer().byIdentity("id1").execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "RemovePeer",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=id1",
+                       "EndMessage"
+               ));
+               fcpServer.writeLine(
+                       "PeerRemoved",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=Friend1",
+                       "EndMessage"
+               );
+               assertThat(peer.get(), is(true));
+       }
+
+       @Test
+       public void defaultFcpClientCanRemovePeerByHostAndPort()
+       throws InterruptedException, ExecutionException, IOException {
+               Future<Boolean> peer = fcpClient.removePeer().byHostAndPort("1.2.3.4", 5678).execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "RemovePeer",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=1.2.3.4:5678",
+                       "EndMessage"
+               ));
+               fcpServer.writeLine(
+                       "PeerRemoved",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=Friend1",
+                       "EndMessage"
+               );
+               assertThat(peer.get(), is(true));
+       }
+
+       @Test
+       public void defaultFcpClientCanModifyPeerNoteByName()
+       throws InterruptedException, ExecutionException, IOException {
+               Future<Boolean> noteUpdated = fcpClient.modifyPeerNote().darknetComment("foo").byName("Friend1").execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "ModifyPeerNote",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=Friend1",
+                       "PeerNoteType=1",
+                       "NoteText=Zm9v",
+                       "EndMessage"
+               ));
+               fcpServer.writeLine(
+                       "PeerNote",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=Friend1",
+                       "NoteText=Zm9v",
+                       "PeerNoteType=1",
+                       "EndMessage"
+               );
+               assertThat(noteUpdated.get(), is(true));
+       }
+
+       @Test
+       public void defaultFcpClientKnowsPeerNoteWasNotModifiedOnUnknownNodeIdentifier()
+       throws InterruptedException, ExecutionException, IOException {
+               Future<Boolean> noteUpdated = fcpClient.modifyPeerNote().darknetComment("foo").byName("Friend1").execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "ModifyPeerNote",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=Friend1",
+                       "PeerNoteType=1",
+                       "NoteText=Zm9v",
+                       "EndMessage"
+               ));
+               fcpServer.writeLine(
+                       "UnknownNodeIdentifier",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=Friend1",
+                       "EndMessage"
+               );
+               assertThat(noteUpdated.get(), is(false));
+       }
+
+       @Test
+       public void defaultFcpClientFailsToModifyPeerNoteWithoutPeerNote()
+       throws InterruptedException, ExecutionException, IOException {
+               Future<Boolean> noteUpdated = fcpClient.modifyPeerNote().byName("Friend1").execute();
+               assertThat(noteUpdated.get(), is(false));
+       }
+
+       @Test
+       public void defaultFcpClientCanModifyPeerNoteByIdentifier()
+       throws InterruptedException, ExecutionException, IOException {
+               Future<Boolean> noteUpdated = fcpClient.modifyPeerNote().darknetComment("foo").byIdentifier("id1").execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "ModifyPeerNote",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=id1",
+                       "PeerNoteType=1",
+                       "NoteText=Zm9v",
+                       "EndMessage"
+               ));
+               fcpServer.writeLine(
+                       "PeerNote",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=id1",
+                       "NoteText=Zm9v",
+                       "PeerNoteType=1",
+                       "EndMessage"
+               );
+               assertThat(noteUpdated.get(), is(true));
+       }
+
+       @Test
+       public void defaultFcpClientCanModifyPeerNoteByHostAndPort()
+       throws InterruptedException, ExecutionException, IOException {
+               Future<Boolean> noteUpdated =
+                       fcpClient.modifyPeerNote().darknetComment("foo").byHostAndPort("1.2.3.4", 5678).execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "ModifyPeerNote",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=1.2.3.4:5678",
+                       "PeerNoteType=1",
+                       "NoteText=Zm9v",
+                       "EndMessage"
+               ));
+               fcpServer.writeLine(
+                       "PeerNote",
+                       "Identifier=" + identifier,
+                       "NodeIdentifier=1.2.3.4:5678",
+                       "NoteText=Zm9v",
+                       "PeerNoteType=1",
+                       "EndMessage"
+               );
+               assertThat(noteUpdated.get(), is(true));
+       }
+
+       @Test
+       public void defaultFcpClientCanGetConfigWithoutDetails()
+       throws InterruptedException, ExecutionException, IOException {
+               Future<ConfigData> configData = fcpClient.getConfig().execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "GetConfig",
+                       "Identifier=" + identifier,
+                       "EndMessage"
+               ));
+               fcpServer.writeLine(
+                       "ConfigData",
+                       "Identifier=" + identifier,
+                       "EndMessage"
+               );
+               assertThat(configData.get(), notNullValue());
+       }
+
+       @Test
+       public void defaultFcpClientCanGetConfigWithCurrent()
+       throws InterruptedException, ExecutionException, IOException {
+               Future<ConfigData> configData = fcpClient.getConfig().withCurrent().execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "GetConfig",
+                       "Identifier=" + identifier,
+                       "WithCurrent=true",
+                       "EndMessage"
+               ));
+               fcpServer.writeLine(
+                       "ConfigData",
+                       "Identifier=" + identifier,
+                       "current.foo=bar",
+                       "EndMessage"
+               );
+               assertThat(configData.get().getCurrent("foo"), is("bar"));
+       }
+
+       @Test
+       public void defaultFcpClientCanGetConfigWithDefaults()
+       throws InterruptedException, ExecutionException, IOException {
+               Future<ConfigData> configData = fcpClient.getConfig().withDefaults().execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "GetConfig",
+                       "Identifier=" + identifier,
+                       "WithDefaults=true",
+                       "EndMessage"
+               ));
+               fcpServer.writeLine(
+                       "ConfigData",
+                       "Identifier=" + identifier,
+                       "default.foo=bar",
+                       "EndMessage"
+               );
+               assertThat(configData.get().getDefault("foo"), is("bar"));
+       }
+
+       @Test
+       public void defaultFcpClientCanGetConfigWithSortOrder()
+       throws InterruptedException, ExecutionException, IOException {
+               Future<ConfigData> configData = fcpClient.getConfig().withSortOrder().execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "GetConfig",
+                       "Identifier=" + identifier,
+                       "WithSortOrder=true",
+                       "EndMessage"
+               ));
+               fcpServer.writeLine(
+                       "ConfigData",
+                       "Identifier=" + identifier,
+                       "sortOrder.foo=17",
+                       "EndMessage"
+               );
+               assertThat(configData.get().getSortOrder("foo"), is(17));
+       }
+
+       @Test
+       public void defaultFcpClientCanGetConfigWithExpertFlag()
+       throws InterruptedException, ExecutionException, IOException {
+               Future<ConfigData> configData = fcpClient.getConfig().withExpertFlag().execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "GetConfig",
+                       "Identifier=" + identifier,
+                       "WithExpertFlag=true",
+                       "EndMessage"
+               ));
+               fcpServer.writeLine(
+                       "ConfigData",
+                       "Identifier=" + identifier,
+                       "expertFlag.foo=true",
+                       "EndMessage"
+               );
+               assertThat(configData.get().getExpertFlag("foo"), is(true));
+       }
+
+       @Test
+       public void defaultFcpClientCanGetConfigWithForceWriteFlag()
+       throws InterruptedException, ExecutionException, IOException {
+               Future<ConfigData> configData = fcpClient.getConfig().withForceWriteFlag().execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "GetConfig",
+                       "Identifier=" + identifier,
+                       "WithForceWriteFlag=true",
+                       "EndMessage"
+               ));
+               fcpServer.writeLine(
+                       "ConfigData",
+                       "Identifier=" + identifier,
+                       "forceWriteFlag.foo=true",
+                       "EndMessage"
+               );
+               assertThat(configData.get().getForceWriteFlag("foo"), is(true));
+       }
+
+       @Test
+       public void defaultFcpClientCanGetConfigWithShortDescription()
+       throws InterruptedException, ExecutionException, IOException {
+               Future<ConfigData> configData = fcpClient.getConfig().withShortDescription().execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "GetConfig",
+                       "Identifier=" + identifier,
+                       "WithShortDescription=true",
+                       "EndMessage"
+               ));
+               fcpServer.writeLine(
+                       "ConfigData",
+                       "Identifier=" + identifier,
+                       "shortDescription.foo=bar",
+                       "EndMessage"
+               );
+               assertThat(configData.get().getShortDescription("foo"), is("bar"));
+       }
+
+       @Test
+       public void defaultFcpClientCanGetConfigWithLongDescription()
+       throws InterruptedException, ExecutionException, IOException {
+               Future<ConfigData> configData = fcpClient.getConfig().withLongDescription().execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "GetConfig",
+                       "Identifier=" + identifier,
+                       "WithLongDescription=true",
+                       "EndMessage"
+               ));
+               fcpServer.writeLine(
+                       "ConfigData",
+                       "Identifier=" + identifier,
+                       "longDescription.foo=bar",
+                       "EndMessage"
+               );
+               assertThat(configData.get().getLongDescription("foo"), is("bar"));
+       }
+
+       @Test
+       public void defaultFcpClientCanGetConfigWithDataTypes()
+       throws InterruptedException, ExecutionException, IOException {
+               Future<ConfigData> configData = fcpClient.getConfig().withDataTypes().execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "GetConfig",
+                       "Identifier=" + identifier,
+                       "WithDataTypes=true",
+                       "EndMessage"
+               ));
+               fcpServer.writeLine(
+                       "ConfigData",
+                       "Identifier=" + identifier,
+                       "dataType.foo=number",
+                       "EndMessage"
+               );
+               assertThat(configData.get().getDataType("foo"), is("number"));
+       }
+
+       @Test
+       public void defaultFcpClientCanModifyConfigData() throws InterruptedException, ExecutionException, IOException {
+               Future<ConfigData> newConfigData = fcpClient.modifyConfig().set("foo.bar").to("baz").execute();
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               String identifier = extractIdentifier(lines);
+               assertThat(lines, matchesFcpMessage(
+                       "ModifyConfig",
+                       "Identifier=" + identifier,
+                       "foo.bar=baz",
+                       "EndMessage"
+               ));
+               fcpServer.writeLine(
+                       "ConfigData",
+                       "Identifier=" + identifier,
+                       "current.foo.bar=baz",
+                       "EndMessage"
+               );
+               assertThat(newConfigData.get().getCurrent("foo.bar"), is("baz"));
+       }
+
+       private List<String> lines;
+       private String identifier;
+
+       private void connectAndAssert(Supplier<Matcher<List<String>>> requestMatcher)
+       throws InterruptedException, ExecutionException, IOException {
+               connectNode();
+               lines = fcpServer.collectUntil(is("EndMessage"));
+               identifier = extractIdentifier(lines);
+               assertThat(lines, requestMatcher.get());
+       }
+
+       public class PluginCommands {
+
+               private static final String CLASS_NAME = "foo.plugin.Plugin";
+
+               private void replyWithPluginInfo() throws IOException {
+                       fcpServer.writeLine(
+                               "PluginInfo",
+                               "Identifier=" + identifier,
+                               "PluginName=superPlugin",
+                               "IsTalkable=true",
+                               "LongVersion=1.2.3",
+                               "Version=42",
+                               "OriginUri=superPlugin",
+                               "Started=true",
+                               "EndMessage"
+                       );
+               }
+
+               private void verifyPluginInfo(Future<Optional<PluginInfo>> pluginInfo)
+               throws InterruptedException, ExecutionException {
+                       assertThat(pluginInfo.get().get().getPluginName(), is("superPlugin"));
+                       assertThat(pluginInfo.get().get().getOriginalURI(), is("superPlugin"));
+                       assertThat(pluginInfo.get().get().isTalkable(), is(true));
+                       assertThat(pluginInfo.get().get().getVersion(), is("42"));
+                       assertThat(pluginInfo.get().get().getLongVersion(), is("1.2.3"));
+                       assertThat(pluginInfo.get().get().isStarted(), is(true));
+               }
+
+               public class LoadPlugin {
+
+                       public class OfficialPlugins {
+
+                               @Test
+                               public void fromFreenet() throws ExecutionException, InterruptedException, IOException {
+                                       Future<Optional<PluginInfo>> pluginInfo =
+                                               fcpClient.loadPlugin().officialFromFreenet("superPlugin").execute();
+                                       connectAndAssert(() -> createMatcherForOfficialSource("freenet"));
+                                       assertThat(lines, not(contains(startsWith("Store="))));
+                                       replyWithPluginInfo();
+                                       verifyPluginInfo(pluginInfo);
+                               }
+
+                               @Test
+                               public void persistentFromFreenet() throws ExecutionException, InterruptedException, IOException {
+                                       Future<Optional<PluginInfo>> pluginInfo =
+                                               fcpClient.loadPlugin().addToConfig().officialFromFreenet("superPlugin").execute();
+                                       connectAndAssert(() -> createMatcherForOfficialSource("freenet"));
+                                       assertThat(lines, hasItem("Store=true"));
+                                       replyWithPluginInfo();
+                                       verifyPluginInfo(pluginInfo);
+                               }
+
+                               @Test
+                               public void fromHttps() throws ExecutionException, InterruptedException, IOException {
+                                       Future<Optional<PluginInfo>> pluginInfo =
+                                               fcpClient.loadPlugin().officialFromHttps("superPlugin").execute();
+                                       connectAndAssert(() -> createMatcherForOfficialSource("https"));
+                                       replyWithPluginInfo();
+                                       verifyPluginInfo(pluginInfo);
+                               }
+
+                               private Matcher<List<String>> createMatcherForOfficialSource(String officialSource) {
+                                       return matchesFcpMessage(
+                                               "LoadPlugin",
+                                               "Identifier=" + identifier,
+                                               "PluginURL=superPlugin",
+                                               "URLType=official",
+                                               "OfficialSource=" + officialSource,
+                                               "EndMessage"
+                                       );
+                               }
+
+                       }
+
+                       public class FromOtherSources {
+
+                               private static final String FILE_PATH = "/path/to/plugin.jar";
+                               private static final String URL = "http://server.com/plugin.jar";
+                               private static final String KEY = "KSK@plugin.jar";
+
+                               @Test
+                               public void fromFile() throws ExecutionException, InterruptedException, IOException {
+                                       Future<Optional<PluginInfo>> pluginInfo = fcpClient.loadPlugin().fromFile(FILE_PATH).execute();
+                                       connectAndAssert(() -> createMatcher("file", FILE_PATH));
+                                       replyWithPluginInfo();
+                                       verifyPluginInfo(pluginInfo);
+                               }
+
+                               @Test
+                               public void fromUrl() throws ExecutionException, InterruptedException, IOException {
+                                       Future<Optional<PluginInfo>> pluginInfo = fcpClient.loadPlugin().fromUrl(URL).execute();
+                                       connectAndAssert(() -> createMatcher("url", URL));
+                                       replyWithPluginInfo();
+                                       verifyPluginInfo(pluginInfo);
+                               }
+
+                               @Test
+                               public void fromFreenet() throws ExecutionException, InterruptedException, IOException {
+                                       Future<Optional<PluginInfo>> pluginInfo = fcpClient.loadPlugin().fromFreenet(KEY).execute();
+                                       connectAndAssert(() -> createMatcher("freenet", KEY));
+                                       replyWithPluginInfo();
+                                       verifyPluginInfo(pluginInfo);
+                               }
+
+                               private Matcher<List<String>> createMatcher(String urlType, String url) {
+                                       return matchesFcpMessage(
+                                               "LoadPlugin",
+                                               "Identifier=" + identifier,
+                                               "PluginURL=" + url,
+                                               "URLType=" + urlType,
+                                               "EndMessage"
+                                       );
+                               }
+
+                       }
+
+                       public class Failed {
+
+                               @Test
+                               public void failedLoad() throws ExecutionException, InterruptedException, IOException {
+                                       Future<Optional<PluginInfo>> pluginInfo =
+                                               fcpClient.loadPlugin().officialFromFreenet("superPlugin").execute();
+                                       connectAndAssert(() -> matchesFcpMessage("LoadPlugin", "EndMessage"));
+                                       replyWithProtocolError();
+                                       assertThat(pluginInfo.get().isPresent(), is(false));
+                               }
+
+                       }
+
+               }
+
+               private void replyWithProtocolError() throws IOException {
+                       fcpServer.writeLine(
+                               "ProtocolError",
+                               "Identifier=" + identifier,
+                               "EndMessage"
+                       );
+               }
+
+               public class ReloadPlugin {
+
+                       @Test
+                       public void reloadingPluginWorks() throws InterruptedException, ExecutionException, IOException {
+                               Future<Optional<PluginInfo>> pluginInfo = fcpClient.reloadPlugin().plugin(CLASS_NAME).execute();
+                               connectAndAssert(() -> matchReloadPluginMessage());
+                               replyWithPluginInfo();
+                               verifyPluginInfo(pluginInfo);
+                       }
+
+                       @Test
+                       public void reloadingPluginWithMaxWaitTimeWorks()
+                       throws InterruptedException, ExecutionException, IOException {
+                               Future<Optional<PluginInfo>> pluginInfo =
+                                       fcpClient.reloadPlugin().waitFor(1234).plugin(CLASS_NAME).execute();
+                               connectAndAssert(() -> allOf(matchReloadPluginMessage(), hasItem("MaxWaitTime=1234")));
+                               replyWithPluginInfo();
+                               verifyPluginInfo(pluginInfo);
+                       }
+
+                       @Test
+                       public void reloadingPluginWithPurgeWorks()
+                       throws InterruptedException, ExecutionException, IOException {
+                               Future<Optional<PluginInfo>> pluginInfo =
+                                       fcpClient.reloadPlugin().purge().plugin(CLASS_NAME).execute();
+                               connectAndAssert(() -> allOf(matchReloadPluginMessage(), hasItem("Purge=true")));
+                               replyWithPluginInfo();
+                               verifyPluginInfo(pluginInfo);
+                       }
+
+                       @Test
+                       public void reloadingPluginWithStoreWorks()
+                       throws InterruptedException, ExecutionException, IOException {
+                               Future<Optional<PluginInfo>> pluginInfo =
+                                       fcpClient.reloadPlugin().addToConfig().plugin(CLASS_NAME).execute();
+                               connectAndAssert(() -> allOf(matchReloadPluginMessage(), hasItem("Store=true")));
+                               replyWithPluginInfo();
+                               verifyPluginInfo(pluginInfo);
+                       }
+
+                       private Matcher<List<String>> matchReloadPluginMessage() {
+                               return matchesFcpMessage(
+                                       "ReloadPlugin",
+                                       "Identifier=" + identifier,
+                                       "PluginName=" + CLASS_NAME,
+                                       "EndMessage"
+                               );
+                       }
+
+               }
+
+               public class RemovePlugin {
+
+                       @Test
+                       public void removingPluginWorks() throws InterruptedException, ExecutionException, IOException {
+                               Future<Boolean> pluginRemoved = fcpClient.removePlugin().plugin(CLASS_NAME).execute();
+                               connectAndAssert(() -> matchPluginRemovedMessage());
+                               replyWithPluginRemoved();
+                               assertThat(pluginRemoved.get(), is(true));
+                       }
+
+                       @Test
+                       public void removingPluginWithMaxWaitTimeWorks()
+                       throws InterruptedException, ExecutionException, IOException {
+                               Future<Boolean> pluginRemoved = fcpClient.removePlugin().waitFor(1234).plugin(CLASS_NAME).execute();
+                               connectAndAssert(() -> allOf(matchPluginRemovedMessage(), hasItem("MaxWaitTime=1234")));
+                               replyWithPluginRemoved();
+                               assertThat(pluginRemoved.get(), is(true));
+                       }
+
+                       @Test
+                       public void removingPluginWithPurgeWorks()
+                       throws InterruptedException, ExecutionException, IOException {
+                               Future<Boolean> pluginRemoved = fcpClient.removePlugin().purge().plugin(CLASS_NAME).execute();
+                               connectAndAssert(() -> allOf(matchPluginRemovedMessage(), hasItem("Purge=true")));
+                               replyWithPluginRemoved();
+                               assertThat(pluginRemoved.get(), is(true));
+                       }
+
+                       private void replyWithPluginRemoved() throws IOException {
+                               fcpServer.writeLine(
+                                       "PluginRemoved",
+                                       "Identifier=" + identifier,
+                                       "PluginName=" + CLASS_NAME,
+                                       "EndMessage"
+                               );
+                       }
+
+                       private Matcher<List<String>> matchPluginRemovedMessage() {
+                               return matchesFcpMessage(
+                                       "RemovePlugin",
+                                       "Identifier=" + identifier,
+                                       "PluginName=" + CLASS_NAME,
+                                       "EndMessage"
+                               );
+                       }
+
+               }
+
+               public class GetPluginInfo {
+
+                       @Test
+                       public void gettingPluginInfoWorks() throws InterruptedException, ExecutionException, IOException {
+                               Future<Optional<PluginInfo>> pluginInfo = fcpClient.getPluginInfo().plugin(CLASS_NAME).execute();
+                               connectAndAssert(() -> matchGetPluginInfoMessage());
+                               replyWithPluginInfo();
+                               verifyPluginInfo(pluginInfo);
+                       }
+
+                       @Test
+                       public void gettingPluginInfoWithDetailsWorks()
+                       throws InterruptedException, ExecutionException, IOException {
+                               Future<Optional<PluginInfo>> pluginInfo =
+                                       fcpClient.getPluginInfo().detailed().plugin(CLASS_NAME).execute();
+                               connectAndAssert(() -> allOf(matchGetPluginInfoMessage(), hasItem("Detailed=true")));
+                               replyWithPluginInfo();
+                               verifyPluginInfo(pluginInfo);
+                       }
+
+                       @Test
+                       public void protocolErrorIsRecognizedAsFailure()
+                       throws InterruptedException, ExecutionException, IOException {
+                               Future<Optional<PluginInfo>> pluginInfo =
+                                       fcpClient.getPluginInfo().detailed().plugin(CLASS_NAME).execute();
+                               connectAndAssert(() -> allOf(matchGetPluginInfoMessage(), hasItem("Detailed=true")));
+                               replyWithProtocolError();
+                               assertThat(pluginInfo.get(), is(Optional.empty()));
+                       }
+
+                       private Matcher<List<String>> matchGetPluginInfoMessage() {
+                               return matchesFcpMessage(
+                                       "GetPluginInfo",
+                                       "Identifier=" + identifier,
+                                       "PluginName=" + CLASS_NAME,
+                                       "EndMessage"
+                               );
+                       }
+
+               }
+
+       }
+
+       public class UskSubscriptionCommands {
+
+               private static final String URI = "USK@some,uri/file.txt";
+
+               @Test
+               public void subscriptionWorks() throws InterruptedException, ExecutionException, IOException {
+                       Future<Optional<UskSubscription>> uskSubscription = fcpClient.subscribeUsk().uri(URI).execute();
+                       connectAndAssert(() -> matchesFcpMessage("SubscribeUSK", "URI=" + URI, "EndMessage"));
+                       replyWithSubscribed();
+                       assertThat(uskSubscription.get().get().getUri(), is(URI));
+                       AtomicInteger edition = new AtomicInteger();
+                       CountDownLatch updated = new CountDownLatch(2);
+                       uskSubscription.get().get().onUpdate(e -> {
+                               edition.set(e);
+                               updated.countDown();
+                       });
+                       sendUpdateNotification(23);
+                       sendUpdateNotification(24);
+                       assertThat("updated in time", updated.await(5, TimeUnit.SECONDS), is(true));
+                       assertThat(edition.get(), is(24));
+               }
+
+               private void replyWithSubscribed() throws IOException {
+                       fcpServer.writeLine(
+                               "SubscribedUSK",
+                               "Identifier=" + identifier,
+                               "URI=" + URI,
+                               "DontPoll=false",
+                               "EndMessage"
+                       );
+               }
+
+               private void sendUpdateNotification(int edition, String... additionalLines) throws IOException {
+                       fcpServer.writeLine(
+                               "SubscribedUSKUpdate",
+                               "Identifier=" + identifier,
+                               "URI=" + URI,
+                               "Edition=" + edition
+                       );
+                       fcpServer.writeLine(additionalLines);
+                       fcpServer.writeLine("EndMessage");
+               }
+
+       }
+
 }