Remove JUnit @Rule, the files don’t have to exist
[jFCPlib.git] / src / test / java / net / pterodactylus / fcp / quelaton / DefaultFcpClientTest.java
index 32837dd..e526f2f 100644 (file)
@@ -1,5 +1,6 @@
 package net.pterodactylus.fcp.quelaton;
 
+import static net.pterodactylus.fcp.RequestProgressMatcher.isRequestProgress;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.allOf;
 import static org.hamcrest.Matchers.contains;
@@ -16,6 +17,7 @@ import java.io.File;
 import java.io.IOException;
 import java.net.URL;
 import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
@@ -43,6 +45,7 @@ import net.pterodactylus.fcp.Peer;
 import net.pterodactylus.fcp.PeerNote;
 import net.pterodactylus.fcp.PluginInfo;
 import net.pterodactylus.fcp.Priority;
+import net.pterodactylus.fcp.RequestProgress;
 import net.pterodactylus.fcp.fake.FakeTcpServer;
 import net.pterodactylus.fcp.quelaton.ClientGetCommand.Data;
 
@@ -55,7 +58,10 @@ import org.hamcrest.Matchers;
 import org.hamcrest.TypeSafeDiagnosingMatcher;
 import org.junit.After;
 import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 import org.junit.runner.RunWith;
 
 /**
@@ -115,67 +121,10 @@ public class DefaultFcpClientTest {
                        .orElse("");
        }
 
-       @Test
-       public void defaultFcpClientReusesConnection() throws InterruptedException, ExecutionException, IOException {
-               Future<FcpKeyPair> keyPair = fcpClient.generateKeypair().execute();
-               connectNode();
-               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
-               String identifier = extractIdentifier(lines);
-               fcpServer.writeLine(
-                       "SSKKeypair",
-                       "InsertURI=" + INSERT_URI + "",
-                       "RequestURI=" + REQUEST_URI + "",
-                       "Identifier=" + identifier,
-                       "EndMessage"
-               );
-               keyPair.get();
-               keyPair = fcpClient.generateKeypair().execute();
-               lines = fcpServer.collectUntil(is("EndMessage"));
-               identifier = extractIdentifier(lines);
-               fcpServer.writeLine(
-                       "SSKKeypair",
-                       "InsertURI=" + INSERT_URI + "",
-                       "RequestURI=" + REQUEST_URI + "",
-                       "Identifier=" + identifier,
-                       "EndMessage"
-               );
-               keyPair.get();
-       }
-
-       @Test
-       public void defaultFcpClientCanReconnectAfterConnectionHasBeenClosed()
-       throws InterruptedException, ExecutionException, IOException {
-               Future<FcpKeyPair> keyPair = fcpClient.generateKeypair().execute();
-               connectNode();
-               fcpServer.collectUntil(is("EndMessage"));
-               fcpServer.close();
-               try {
-                       keyPair.get();
-                       Assert.fail();
-               } catch (ExecutionException e) {
-               }
-               keyPair = fcpClient.generateKeypair().execute();
-               connectNode();
-               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
-               String identifier = extractIdentifier(lines);
-               fcpServer.writeLine(
-                       "SSKKeypair",
-                       "InsertURI=" + INSERT_URI + "",
-                       "RequestURI=" + REQUEST_URI + "",
-                       "Identifier=" + identifier,
-                       "EndMessage"
-               );
-               keyPair.get();
-       }
-
        private Matcher<List<String>> matchesFcpMessage(String name, String... requiredLines) {
                return matchesFcpMessageWithTerminator(name, "EndMessage", requiredLines);
        }
 
-       private Matcher<List<String>> matchesDataMessage(String name, String... requiredLines) {
-               return matchesFcpMessageWithTerminator(name, "Data", requiredLines);
-       }
-
        private Matcher<Iterable<String>> hasHead(String firstElement) {
                return new TypeSafeDiagnosingMatcher<Iterable<String>>() {
                        @Override
@@ -253,700 +202,102 @@ public class DefaultFcpClientTest {
                };
        }
 
-       @Test
-       public void clientPutWithDirectDataSendsCorrectCommand()
-       throws IOException, ExecutionException, InterruptedException {
-               fcpClient.clientPut()
-                       .from(new ByteArrayInputStream("Hello\n".getBytes()))
-                       .length(6)
-                       .uri("KSK@foo.txt")
-                       .execute();
-               connectNode();
-               List<String> lines = fcpServer.collectUntil(is("Hello"));
-               assertThat(lines, allOf(
-                       hasHead("ClientPut"),
-                       hasParameters(1, 2, "UploadFrom=direct", "DataLength=6", "URI=KSK@foo.txt"),
-                       hasTail("EndMessage", "Hello")
-               ));
-       }
-
-       @Test
-       public void clientPutWithDirectDataSucceedsOnCorrectIdentifier()
-       throws InterruptedException, ExecutionException, IOException {
-               Future<Optional<Key>> key = fcpClient.clientPut()
-                       .from(new ByteArrayInputStream("Hello\n".getBytes()))
-                       .length(6)
-                       .uri("KSK@foo.txt")
-                       .execute();
-               connectNode();
-               List<String> lines = fcpServer.collectUntil(is("Hello"));
-               String identifier = extractIdentifier(lines);
-               fcpServer.writeLine(
-                       "PutFailed",
-                       "Identifier=not-the-right-one",
-                       "EndMessage"
-               );
-               fcpServer.writeLine(
-                       "PutSuccessful",
-                       "URI=KSK@foo.txt",
-                       "Identifier=" + identifier,
-                       "EndMessage"
-               );
-               assertThat(key.get().get().getKey(), is("KSK@foo.txt"));
-       }
-
-       @Test
-       public void clientPutWithDirectDataFailsOnCorrectIdentifier()
-       throws InterruptedException, ExecutionException, IOException {
-               Future<Optional<Key>> key = fcpClient.clientPut()
-                       .from(new ByteArrayInputStream("Hello\n".getBytes()))
-                       .length(6)
-                       .uri("KSK@foo.txt")
-                       .execute();
-               connectNode();
-               List<String> lines = fcpServer.collectUntil(is("Hello"));
-               String identifier = extractIdentifier(lines);
-               fcpServer.writeLine(
-                       "PutSuccessful",
-                       "Identifier=not-the-right-one",
-                       "URI=KSK@foo.txt",
-                       "EndMessage"
-               );
-               fcpServer.writeLine(
-                       "PutFailed",
-                       "Identifier=" + identifier,
-                       "EndMessage"
-               );
-               assertThat(key.get().isPresent(), is(false));
-       }
-
-       @Test
-       public void clientPutWithRenamedDirectDataSendsCorrectCommand()
-       throws InterruptedException, ExecutionException, IOException {
-               fcpClient.clientPut()
-                       .named("otherName.txt")
-                       .from(new ByteArrayInputStream("Hello\n".getBytes()))
-                       .length(6)
-                       .uri("KSK@foo.txt")
-                       .execute();
-               connectNode();
-               List<String> lines = fcpServer.collectUntil(is("Hello"));
-               assertThat(lines, allOf(
-                       hasHead("ClientPut"),
-                       hasParameters(1, 2, "TargetFilename=otherName.txt", "UploadFrom=direct", "DataLength=6", "URI=KSK@foo.txt"),
-                       hasTail("EndMessage", "Hello")
-               ));
-       }
-
-       @Test
-       public void clientPutWithRedirectSendsCorrectCommand()
-       throws IOException, ExecutionException, InterruptedException {
-               fcpClient.clientPut().redirectTo("KSK@bar.txt").uri("KSK@foo.txt").execute();
-               connectNode();
-               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
-               assertThat(lines,
-                       matchesFcpMessage("ClientPut", "UploadFrom=redirect", "URI=KSK@foo.txt", "TargetURI=KSK@bar.txt"));
-       }
-
-       @Test
-       public void clientPutWithFileSendsCorrectCommand() throws InterruptedException, ExecutionException, IOException {
-               fcpClient.clientPut().from(new File("/tmp/data.txt")).uri("KSK@foo.txt").execute();
-               connectNode();
-               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
-               assertThat(lines,
-                       matchesFcpMessage("ClientPut", "UploadFrom=disk", "URI=KSK@foo.txt", "Filename=/tmp/data.txt"));
-       }
-
-       @Test
-       public void clientPutWithFileCanCompleteTestDdaSequence()
-       throws IOException, ExecutionException, InterruptedException {
-               File tempFile = createTempFile();
-               fcpClient.clientPut().from(new File(tempFile.getParent(), "test.dat")).uri("KSK@foo.txt").execute();
-               connectNode();
-               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
-               String identifier = extractIdentifier(lines);
-               fcpServer.writeLine(
-                       "ProtocolError",
-                       "Identifier=" + identifier,
-                       "Code=25",
-                       "EndMessage"
-               );
-               lines = fcpServer.collectUntil(is("EndMessage"));
-               assertThat(lines, matchesFcpMessage(
-                       "TestDDARequest",
-                       "Directory=" + tempFile.getParent(),
-                       "WantReadDirectory=true",
-                       "WantWriteDirectory=false"
-               ));
-               fcpServer.writeLine(
-                       "TestDDAReply",
-                       "Directory=" + tempFile.getParent(),
-                       "ReadFilename=" + tempFile,
-                       "EndMessage"
-               );
-               lines = fcpServer.collectUntil(is("EndMessage"));
-               assertThat(lines, matchesFcpMessage(
-                       "TestDDAResponse",
-                       "Directory=" + tempFile.getParent(),
-                       "ReadContent=test-content"
-               ));
-               fcpServer.writeLine(
-                       "TestDDAComplete",
-                       "Directory=" + tempFile.getParent(),
-                       "ReadDirectoryAllowed=true",
-                       "EndMessage"
-               );
-               lines = fcpServer.collectUntil(is("EndMessage"));
-               assertThat(lines,
-                       matchesFcpMessage("ClientPut", "UploadFrom=disk", "URI=KSK@foo.txt",
-                               "Filename=" + new File(tempFile.getParent(), "test.dat")));
-       }
-
-       private File createTempFile() throws IOException {
-               File tempFile = File.createTempFile("test-dda-", ".dat");
-               tempFile.deleteOnExit();
-               Files.write("test-content", tempFile, StandardCharsets.UTF_8);
-               return tempFile;
-       }
-
-       @Test
-       public void clientPutDoesNotReactToProtocolErrorForDifferentIdentifier()
-       throws InterruptedException, ExecutionException, IOException {
-               Future<Optional<Key>> key = fcpClient.clientPut().from(new File("/tmp/data.txt")).uri("KSK@foo.txt").execute();
-               connectNode();
-               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
-               String identifier = extractIdentifier(lines);
-               fcpServer.writeLine(
-                       "ProtocolError",
-                       "Identifier=not-the-right-one",
-                       "Code=25",
-                       "EndMessage"
-               );
-               fcpServer.writeLine(
-                       "PutSuccessful",
-                       "Identifier=" + identifier,
-                       "URI=KSK@foo.txt",
-                       "EndMessage"
-               );
-               assertThat(key.get().get().getKey(), is("KSK@foo.txt"));
-       }
+       private List<String> lines;
+       private String identifier;
 
-       @Test
-       public void clientPutAbortsOnProtocolErrorOtherThan25()
+       private void connectAndAssert(Supplier<Matcher<List<String>>> requestMatcher)
        throws InterruptedException, ExecutionException, IOException {
-               Future<Optional<Key>> key = fcpClient.clientPut().from(new File("/tmp/data.txt")).uri("KSK@foo.txt").execute();
                connectNode();
-               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
-               String identifier = extractIdentifier(lines);
-               fcpServer.writeLine(
-                       "ProtocolError",
-                       "Identifier=" + identifier,
-                       "Code=1",
-                       "EndMessage"
-               );
-               assertThat(key.get().isPresent(), is(false));
+               readMessage(requestMatcher);
        }
 
-       @Test
-       public void clientPutDoesNotReplyToWrongTestDdaReply() throws IOException, ExecutionException,
-       InterruptedException {
-               File tempFile = createTempFile();
-               fcpClient.clientPut().from(new File(tempFile.getParent(), "test.dat")).uri("KSK@foo.txt").execute();
-               connectNode();
-               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
-               String identifier = extractIdentifier(lines);
-               fcpServer.writeLine(
-                       "ProtocolError",
-                       "Identifier=" + identifier,
-                       "Code=25",
-                       "EndMessage"
-               );
-               lines = fcpServer.collectUntil(is("EndMessage"));
-               assertThat(lines, matchesFcpMessage(
-                       "TestDDARequest",
-                       "Directory=" + tempFile.getParent(),
-                       "WantReadDirectory=true",
-                       "WantWriteDirectory=false"
-               ));
-               fcpServer.writeLine(
-                       "TestDDAReply",
-                       "Directory=/some-other-directory",
-                       "ReadFilename=" + tempFile,
-                       "EndMessage"
-               );
-               fcpServer.writeLine(
-                       "TestDDAReply",
-                       "Directory=" + tempFile.getParent(),
-                       "ReadFilename=" + tempFile,
-                       "EndMessage"
-               );
-               lines = fcpServer.collectUntil(is("EndMessage"));
-               assertThat(lines, matchesFcpMessage(
-                       "TestDDAResponse",
-                       "Directory=" + tempFile.getParent(),
-                       "ReadContent=test-content"
-               ));
+       private void readMessage(Supplier<Matcher<List<String>>> requestMatcher) throws IOException {
+               readMessage("EndMessage", requestMatcher);
        }
 
-       @Test
-       public void clientPutSendsResponseEvenIfFileCanNotBeRead()
-       throws IOException, ExecutionException, InterruptedException {
-               File tempFile = createTempFile();
-               fcpClient.clientPut().from(new File(tempFile.getParent(), "test.dat")).uri("KSK@foo.txt").execute();
-               connectNode();
-               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
-               String identifier = extractIdentifier(lines);
-               fcpServer.writeLine(
-                       "ProtocolError",
-                       "Identifier=" + identifier,
-                       "Code=25",
-                       "EndMessage"
-               );
-               lines = fcpServer.collectUntil(is("EndMessage"));
-               assertThat(lines, matchesFcpMessage(
-                       "TestDDARequest",
-                       "Directory=" + tempFile.getParent(),
-                       "WantReadDirectory=true",
-                       "WantWriteDirectory=false"
-               ));
-               fcpServer.writeLine(
-                       "TestDDAReply",
-                       "Directory=" + tempFile.getParent(),
-                       "ReadFilename=" + tempFile + ".foo",
-                       "EndMessage"
-               );
-               lines = fcpServer.collectUntil(is("EndMessage"));
-               assertThat(lines, matchesFcpMessage(
-                       "TestDDAResponse",
-                       "Directory=" + tempFile.getParent(),
-                       "ReadContent=failed-to-read"
-               ));
+       private void readMessage(String terminator, Supplier<Matcher<List<String>>> requestMatcher) throws IOException {
+               lines = fcpServer.collectUntil(is(terminator));
+               identifier = extractIdentifier(lines);
+               assertThat(lines, requestMatcher.get());
        }
 
-       @Test
-       public void clientPutDoesNotResendOriginalClientPutOnTestDDACompleteWithWrongDirectory()
-       throws IOException, ExecutionException, InterruptedException {
-               File tempFile = createTempFile();
-               fcpClient.clientPut().from(new File(tempFile.getParent(), "test.dat")).uri("KSK@foo.txt").execute();
-               connectNode();
-               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
-               String identifier = extractIdentifier(lines);
-               fcpServer.writeLine(
-                       "TestDDAComplete",
-                       "Directory=/some-other-directory",
-                       "EndMessage"
-               );
+       private void replyWithProtocolError() throws IOException {
                fcpServer.writeLine(
                        "ProtocolError",
                        "Identifier=" + identifier,
-                       "Code=25",
-                       "EndMessage"
-               );
-               lines = fcpServer.collectUntil(is("EndMessage"));
-               assertThat(lines, matchesFcpMessage(
-                       "TestDDARequest",
-                       "Directory=" + tempFile.getParent(),
-                       "WantReadDirectory=true",
-                       "WantWriteDirectory=false"
-               ));
-       }
-
-       @Test
-       public void clientPutSendsNotificationsForGeneratedKeys()
-       throws InterruptedException, ExecutionException, IOException {
-               List<String> generatedKeys = new CopyOnWriteArrayList<>();
-               Future<Optional<Key>> key = fcpClient.clientPut()
-                       .onKeyGenerated(generatedKeys::add)
-                       .from(new ByteArrayInputStream("Hello\n".getBytes()))
-                       .length(6)
-                       .uri("KSK@foo.txt")
-                       .execute();
-               connectNode();
-               List<String> lines = fcpServer.collectUntil(is("Hello"));
-               String identifier = extractIdentifier(lines);
-               fcpServer.writeLine(
-                       "URIGenerated",
-                       "Identifier=" + identifier,
-                       "URI=KSK@foo.txt",
-                       "EndMessage"
-               );
-               fcpServer.writeLine(
-                       "PutSuccessful",
-                       "URI=KSK@foo.txt",
-                       "Identifier=" + identifier,
-                       "EndMessage"
-               );
-               assertThat(key.get().get().getKey(), is("KSK@foo.txt"));
-               assertThat(generatedKeys, contains("KSK@foo.txt"));
-       }
-
-       @Test
-       public void defaultFcpClientCanGetNodeInformation() throws InterruptedException, ExecutionException, IOException {
-               Future<NodeData> nodeData = fcpClient.getNode().execute();
-               connectNode();
-               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
-               String identifier = extractIdentifier(lines);
-               assertThat(lines, matchesFcpMessage(
-                       "GetNode",
-                       "Identifier=" + identifier,
-                       "GiveOpennetRef=false",
-                       "WithPrivate=false",
-                       "WithVolatile=false"
-               ));
-               fcpServer.writeLine(
-                       "NodeData",
-                       "Identifier=" + identifier,
-                       "ark.pubURI=SSK@3YEf.../ark",
-                       "ark.number=78",
-                       "auth.negTypes=2",
-                       "version=Fred,0.7,1.0,1466",
-                       "lastGoodVersion=Fred,0.7,1.0,1466",
-                       "EndMessage"
-               );
-               assertThat(nodeData.get(), notNullValue());
-       }
-
-       @Test
-       public void defaultFcpClientCanGetNodeInformationWithOpennetRef()
-       throws InterruptedException, ExecutionException, IOException {
-               Future<NodeData> nodeData = fcpClient.getNode().opennetRef().execute();
-               connectNode();
-               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
-               String identifier = extractIdentifier(lines);
-               assertThat(lines, matchesFcpMessage(
-                       "GetNode",
-                       "Identifier=" + identifier,
-                       "GiveOpennetRef=true",
-                       "WithPrivate=false",
-                       "WithVolatile=false"
-               ));
-               fcpServer.writeLine(
-                       "NodeData",
-                       "Identifier=" + identifier,
-                       "opennet=true",
-                       "ark.pubURI=SSK@3YEf.../ark",
-                       "ark.number=78",
-                       "auth.negTypes=2",
-                       "version=Fred,0.7,1.0,1466",
-                       "lastGoodVersion=Fred,0.7,1.0,1466",
-                       "EndMessage"
-               );
-               assertThat(nodeData.get().getVersion().toString(), is("Fred,0.7,1.0,1466"));
-       }
-
-       @Test
-       public void defaultFcpClientCanGetNodeInformationWithPrivateData()
-       throws InterruptedException, ExecutionException, IOException {
-               Future<NodeData> nodeData = fcpClient.getNode().includePrivate().execute();
-               connectNode();
-               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
-               String identifier = extractIdentifier(lines);
-               assertThat(lines, matchesFcpMessage(
-                       "GetNode",
-                       "Identifier=" + identifier,
-                       "GiveOpennetRef=false",
-                       "WithPrivate=true",
-                       "WithVolatile=false"
-               ));
-               fcpServer.writeLine(
-                       "NodeData",
-                       "Identifier=" + identifier,
-                       "opennet=false",
-                       "ark.pubURI=SSK@3YEf.../ark",
-                       "ark.number=78",
-                       "auth.negTypes=2",
-                       "version=Fred,0.7,1.0,1466",
-                       "lastGoodVersion=Fred,0.7,1.0,1466",
-                       "ark.privURI=SSK@XdHMiRl",
-                       "EndMessage"
-               );
-               assertThat(nodeData.get().getARK().getPrivateURI(), is("SSK@XdHMiRl"));
-       }
-
-       @Test
-       public void defaultFcpClientCanGetNodeInformationWithVolatileData()
-       throws InterruptedException, ExecutionException, IOException {
-               Future<NodeData> nodeData = fcpClient.getNode().includeVolatile().execute();
-               connectNode();
-               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
-               String identifier = extractIdentifier(lines);
-               assertThat(lines, matchesFcpMessage(
-                       "GetNode",
-                       "Identifier=" + identifier,
-                       "GiveOpennetRef=false",
-                       "WithPrivate=false",
-                       "WithVolatile=true"
-               ));
-               fcpServer.writeLine(
-                       "NodeData",
-                       "Identifier=" + identifier,
-                       "opennet=false",
-                       "ark.pubURI=SSK@3YEf.../ark",
-                       "ark.number=78",
-                       "auth.negTypes=2",
-                       "version=Fred,0.7,1.0,1466",
-                       "lastGoodVersion=Fred,0.7,1.0,1466",
-                       "volatile.freeJavaMemory=205706528",
-                       "EndMessage"
-               );
-               assertThat(nodeData.get().getVolatile("freeJavaMemory"), is("205706528"));
-       }
-
-       @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
-               ));
-               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"
-               ));
-               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"
-               ));
-               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"
-               ));
-               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"
-               ));
-               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"
-               ));
-               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"
-               ));
-               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"
-               ));
-               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"
-               ));
-               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"
-               ));
-               fcpServer.writeLine(
-                       "ConfigData",
-                       "Identifier=" + identifier,
-                       "current.foo.bar=baz",
-                       "EndMessage"
-               );
-               assertThat(newConfigData.get().getCurrent("foo.bar"), is("baz"));
-       }
+       public class ConnectionsAndKeyPairs {
 
-       private List<String> lines;
-       private String identifier;
+               public class Connections {
 
-       private void connectAndAssert(Supplier<Matcher<List<String>>> requestMatcher)
-       throws InterruptedException, ExecutionException, IOException {
-               connectNode();
-               readMessage(requestMatcher);
-       }
+                       @Test(expected = ExecutionException.class)
+                       public void throwsExceptionOnFailure() throws IOException, ExecutionException, InterruptedException {
+                               Future<FcpKeyPair> keyPairFuture = fcpClient.generateKeypair().execute();
+                               connectAndAssert(() -> matchesFcpMessage("GenerateSSK"));
+                               fcpServer.writeLine(
+                                       "CloseConnectionDuplicateClientName",
+                                       "EndMessage"
+                               );
+                               keyPairFuture.get();
+                       }
 
-       private void readMessage(Supplier<Matcher<List<String>>> requestMatcher) throws IOException {
-               lines = fcpServer.collectUntil(is("EndMessage"));
-               identifier = extractIdentifier(lines);
-               assertThat(lines, requestMatcher.get());
-       }
+                       @Test(expected = ExecutionException.class)
+                       public void throwsExceptionIfConnectionIsClosed() throws IOException, ExecutionException, InterruptedException {
+                               Future<FcpKeyPair> keyPairFuture = fcpClient.generateKeypair().execute();
+                               connectAndAssert(() -> matchesFcpMessage("GenerateSSK"));
+                               fcpServer.close();
+                               keyPairFuture.get();
+                       }
 
-       public class Connections {
+                       @Test
+                       public void connectionIsReused() throws InterruptedException, ExecutionException, IOException {
+                               Future<FcpKeyPair> keyPair = fcpClient.generateKeypair().execute();
+                               connectAndAssert(() -> matchesFcpMessage("GenerateSSK"));
+                               replyWithKeyPair();
+                               keyPair.get();
+                               keyPair = fcpClient.generateKeypair().execute();
+                               readMessage(() -> matchesFcpMessage("GenerateSSK"));
+                               identifier = extractIdentifier(lines);
+                               replyWithKeyPair();
+                               keyPair.get();
+                       }
 
-               @Test(expected = ExecutionException.class)
-               public void throwsExceptionOnFailure() throws IOException, ExecutionException, InterruptedException {
-                       Future<FcpKeyPair> keyPairFuture = fcpClient.generateKeypair().execute();
-                       connectAndAssert(() -> matchesFcpMessage("GenerateSSK"));
-                       fcpServer.writeLine(
-                               "CloseConnectionDuplicateClientName",
-                               "EndMessage"
-                       );
-                       keyPairFuture.get();
-               }
+                       @Test
+                       public void defaultFcpClientCanReconnectAfterConnectionHasBeenClosed()
+                       throws InterruptedException, ExecutionException, IOException {
+                               Future<FcpKeyPair> keyPair = fcpClient.generateKeypair().execute();
+                               connectAndAssert(() -> matchesFcpMessage("GenerateSSK"));
+                               fcpServer.close();
+                               try {
+                                       keyPair.get();
+                                       Assert.fail();
+                               } catch (ExecutionException e) {
+                                       /* ignore. */
+                               }
+                               keyPair = fcpClient.generateKeypair().execute();
+                               connectAndAssert(() -> matchesFcpMessage("GenerateSSK"));
+                               replyWithKeyPair();
+                               keyPair.get();
+                       }
 
-               @Test(expected = ExecutionException.class)
-               public void throwsExceptionIfConnectionIsClosed() throws IOException, ExecutionException, InterruptedException {
-                       Future<FcpKeyPair> keyPairFuture = fcpClient.generateKeypair().execute();
-                       connectAndAssert(() -> matchesFcpMessage("GenerateSSK"));
-                       fcpServer.close();
-                       keyPairFuture.get();
                }
 
-       }
+               public class GenerateKeyPair {
 
-       public class GenerateKeyPair {
+                       @Test
+                       public void defaultFcpClientCanGenerateKeypair()
+                       throws ExecutionException, InterruptedException, IOException {
+                               Future<FcpKeyPair> keyPairFuture = fcpClient.generateKeypair().execute();
+                               connectAndAssert(() -> matchesFcpMessage("GenerateSSK"));
+                               replyWithKeyPair();
+                               FcpKeyPair keyPair = keyPairFuture.get();
+                               assertThat(keyPair.getPublicKey(), is(REQUEST_URI));
+                               assertThat(keyPair.getPrivateKey(), is(INSERT_URI));
+                       }
 
-               @Test
-               public void defaultFcpClientCanGenerateKeypair() throws ExecutionException, InterruptedException, IOException {
-                       Future<FcpKeyPair> keyPairFuture = fcpClient.generateKeypair().execute();
-                       connectAndAssert(() -> matchesFcpMessage("GenerateSSK"));
-                       replyWithKeyPair();
-                       FcpKeyPair keyPair = keyPairFuture.get();
-                       assertThat(keyPair.getPublicKey(), is(REQUEST_URI));
-                       assertThat(keyPair.getPrivateKey(), is(INSERT_URI));
                }
 
                private void replyWithKeyPair() throws IOException {
@@ -1101,6 +452,14 @@ public class DefaultFcpClientTest {
                                        assertThat(peer.get().get().getIdentity(), is("id1"));
                                }
 
+                               @Test
+                               public void protocolErrorEndsCommand() throws InterruptedException, ExecutionException, IOException {
+                                       Future<Optional<Peer>> peer = fcpClient.addPeer().fromFile(new File("/tmp/ref.txt")).execute();
+                                       connectAndAssert(() -> allOf(matchesAddPeer(), hasItem("File=/tmp/ref.txt")));
+                                       replyWithProtocolError();
+                                       assertThat(peer.get().isPresent(), is(false));
+                               }
+
                                private NodeRef createNodeRef() {
                                        NodeRef nodeRef = new NodeRef();
                                        nodeRef.setIdentity("id1");
@@ -1637,20 +996,12 @@ public class DefaultFcpClientTest {
 
                }
 
-               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());
+                               connectAndAssert(this::matchReloadPluginMessage);
                                replyWithPluginInfo();
                                verifyPluginInfo(pluginInfo);
                        }
@@ -1685,6 +1036,15 @@ public class DefaultFcpClientTest {
                                verifyPluginInfo(pluginInfo);
                        }
 
+                       @Test
+                       public void protocolErrorIsRecognizedAsFailure()
+                       throws InterruptedException, ExecutionException, IOException {
+                               Future<Optional<PluginInfo>> pluginInfo = fcpClient.reloadPlugin().plugin(CLASS_NAME).execute();
+                               connectAndAssert(() -> matchReloadPluginMessage());
+                               replyWithProtocolError();
+                               assertThat(pluginInfo.get().isPresent(), is(false));
+                       }
+
                        private Matcher<List<String>> matchReloadPluginMessage() {
                                return matchesFcpMessage(
                                        "ReloadPlugin",
@@ -1700,7 +1060,7 @@ public class DefaultFcpClientTest {
                        @Test
                        public void removingPluginWorks() throws InterruptedException, ExecutionException, IOException {
                                Future<Boolean> pluginRemoved = fcpClient.removePlugin().plugin(CLASS_NAME).execute();
-                               connectAndAssert(() -> matchPluginRemovedMessage());
+                               connectAndAssert(this::matchPluginRemovedMessage);
                                replyWithPluginRemoved();
                                assertThat(pluginRemoved.get(), is(true));
                        }
@@ -1747,7 +1107,7 @@ public class DefaultFcpClientTest {
                        @Test
                        public void gettingPluginInfoWorks() throws InterruptedException, ExecutionException, IOException {
                                Future<Optional<PluginInfo>> pluginInfo = fcpClient.getPluginInfo().plugin(CLASS_NAME).execute();
-                               connectAndAssert(() -> matchGetPluginInfoMessage());
+                               connectAndAssert(this::matchGetPluginInfoMessage);
                                replyWithPluginInfo();
                                verifyPluginInfo(pluginInfo);
                        }
@@ -1866,7 +1226,7 @@ public class DefaultFcpClientTest {
                @Test
                public void works() throws InterruptedException, ExecutionException, IOException {
                        Future<Optional<Data>> dataFuture = fcpClient.clientGet().uri("KSK@foo.txt").execute();
-                       connectAndAssert(() -> matchesFcpMessage("ClientGet", "URI=KSK@foo.txt"));
+                       connectAndAssert(() -> matchesFcpMessage("ClientGet", "URI=KSK@foo.txt", "ReturnType=direct"));
                        replyWithAllData("not-test", "Hello World", "text/plain;charset=latin-9");
                        replyWithAllData(identifier, "Hello", "text/plain;charset=utf-8");
                        Optional<Data> data = dataFuture.get();
@@ -1877,6 +1237,7 @@ public class DefaultFcpClientTest {
                public void getFailedIsRecognized() throws InterruptedException, ExecutionException, IOException {
                        Future<Optional<Data>> dataFuture = fcpClient.clientGet().uri("KSK@foo.txt").execute();
                        connectAndAssert(() -> matchesFcpMessage("ClientGet", "URI=KSK@foo.txt"));
+                       replyWithGetFailed("not-test");
                        replyWithGetFailed(identifier);
                        Optional<Data> data = dataFuture.get();
                        assertThat(data.isPresent(), is(false));
@@ -1972,4 +1333,550 @@ public class DefaultFcpClientTest {
 
        }
 
+       public class ClientPut {
+
+               @Test
+               public void sendsCorrectCommand() throws IOException, ExecutionException, InterruptedException {
+                       fcpClient.clientPut()
+                               .from(new ByteArrayInputStream("Hello\n".getBytes()))
+                               .length(6)
+                               .uri("KSK@foo.txt")
+                               .execute();
+                       connectNode();
+                       readMessage("Hello", this::matchesDirectClientPut);
+               }
+
+               @Test
+               public void succeedsOnCorrectIdentifier() throws InterruptedException, ExecutionException, IOException {
+                       Future<Optional<Key>> key = fcpClient.clientPut()
+                               .from(new ByteArrayInputStream("Hello\n".getBytes()))
+                               .length(6)
+                               .uri("KSK@foo.txt")
+                               .execute();
+                       connectNode();
+                       readMessage("Hello", this::matchesDirectClientPut);
+                       replyWithPutFailed("not-the-right-one");
+                       replyWithPutSuccessful(identifier);
+                       assertThat(key.get().get().getKey(), is("KSK@foo.txt"));
+               }
+
+               @Test
+               public void failsOnCorrectIdentifier() throws InterruptedException, ExecutionException, IOException {
+                       Future<Optional<Key>> key = fcpClient.clientPut()
+                               .from(new ByteArrayInputStream("Hello\n".getBytes()))
+                               .length(6)
+                               .uri("KSK@foo.txt")
+                               .execute();
+                       connectNode();
+                       readMessage("Hello", this::matchesDirectClientPut);
+                       replyWithPutSuccessful("not-the-right-one");
+                       replyWithPutFailed(identifier);
+                       assertThat(key.get().isPresent(), is(false));
+               }
+
+               @Test
+               public void renameIsSentCorrectly() throws InterruptedException, ExecutionException, IOException {
+                       fcpClient.clientPut()
+                               .named("otherName.txt")
+                               .from(new ByteArrayInputStream("Hello\n".getBytes()))
+                               .length(6)
+                               .uri("KSK@foo.txt")
+                               .execute();
+                       connectNode();
+                       readMessage("Hello", () -> allOf(
+                               hasHead("ClientPut"),
+                               hasParameters(1, 2, "TargetFilename=otherName.txt", "UploadFrom=direct", "DataLength=6",
+                                       "URI=KSK@foo.txt"),
+                               hasTail("EndMessage", "Hello")
+                       ));
+               }
+
+               @Test
+               public void redirectIsSentCorrecly() throws IOException, ExecutionException, InterruptedException {
+                       fcpClient.clientPut().redirectTo("KSK@bar.txt").uri("KSK@foo.txt").execute();
+                       connectAndAssert(() ->
+                               matchesFcpMessage("ClientPut", "UploadFrom=redirect", "URI=KSK@foo.txt", "TargetURI=KSK@bar.txt"));
+               }
+
+               @Test
+               public void withFileIsSentCorrectly() throws InterruptedException, ExecutionException, IOException {
+                       fcpClient.clientPut().from(new File("/tmp/data.txt")).uri("KSK@foo.txt").execute();
+                       connectAndAssert(() ->
+                               matchesFcpMessage("ClientPut", "UploadFrom=disk", "URI=KSK@foo.txt", "Filename=/tmp/data.txt"));
+               }
+
+               public class DDA {
+
+                       private final File ddaFile;
+                       private final File fileToUpload;
+
+                       public DDA() throws IOException {
+                               ddaFile = createDdaFile();
+                               fileToUpload = new File(ddaFile.getParent(), "test.dat");
+                       }
+
+                       private Matcher<List<String>> matchesFileClientPut(File file) {
+                               return matchesFcpMessage("ClientPut", "UploadFrom=disk", "URI=KSK@foo.txt", "Filename=" + file);
+                       }
+
+                       @Test
+                       public void completeDda() throws IOException, ExecutionException, InterruptedException {
+                               fcpClient.clientPut().from(fileToUpload).uri("KSK@foo.txt").execute();
+                               connectAndAssert(() -> matchesFileClientPut(fileToUpload));
+                               sendDdaRequired(identifier);
+                               readMessage(() -> matchesTestDDARequest(ddaFile));
+                               sendTestDDAReply(ddaFile.getParent(), ddaFile);
+                               readMessage(() -> matchesTestDDAResponse(ddaFile));
+                               writeTestDDAComplete(ddaFile);
+                               readMessage(() -> matchesFileClientPut(fileToUpload));
+                       }
+
+                       @Test
+                       public void ignoreOtherDda() throws IOException, ExecutionException, InterruptedException {
+                               fcpClient.clientPut().from(fileToUpload).uri("KSK@foo.txt").execute();
+                               connectAndAssert(() -> matchesFileClientPut(fileToUpload));
+                               sendDdaRequired(identifier);
+                               readMessage(() -> matchesTestDDARequest(ddaFile));
+                               sendTestDDAReply("/some-other-directory", ddaFile);
+                               sendTestDDAReply(ddaFile.getParent(), ddaFile);
+                               readMessage(() -> matchesTestDDAResponse(ddaFile));
+                       }
+
+                       @Test
+                       public void sendResponseIfFileUnreadable() throws IOException, ExecutionException, InterruptedException {
+                               fcpClient.clientPut().from(fileToUpload).uri("KSK@foo.txt").execute();
+                               connectAndAssert(() -> matchesFileClientPut(fileToUpload));
+                               sendDdaRequired(identifier);
+                               readMessage(() -> matchesTestDDARequest(ddaFile));
+                               sendTestDDAReply(ddaFile.getParent(), new File(ddaFile + ".foo"));
+                               readMessage(this::matchesFailedToReadResponse);
+                       }
+
+                       @Test
+                       public void clientPutDoesNotResendOriginalClientPutOnTestDDACompleteWithWrongDirectory()
+                       throws IOException, ExecutionException, InterruptedException {
+                               fcpClient.clientPut().from(fileToUpload).uri("KSK@foo.txt").execute();
+                               connectNode();
+                               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+                               String identifier = extractIdentifier(lines);
+                               fcpServer.writeLine(
+                                       "TestDDAComplete",
+                                       "Directory=/some-other-directory",
+                                       "EndMessage"
+                               );
+                               sendDdaRequired(identifier);
+                               lines = fcpServer.collectUntil(is("EndMessage"));
+                               assertThat(lines, matchesFcpMessage(
+                                       "TestDDARequest",
+                                       "Directory=" + ddaFile.getParent(),
+                                       "WantReadDirectory=true",
+                                       "WantWriteDirectory=false"
+                               ));
+                       }
+
+                       private Matcher<List<String>> matchesFailedToReadResponse() {
+                               return matchesFcpMessage(
+                                       "TestDDAResponse",
+                                       "Directory=" + ddaFile.getParent(),
+                                       "ReadContent=failed-to-read"
+                               );
+                       }
+
+                       private void writeTestDDAComplete(File tempFile) throws IOException {
+                               fcpServer.writeLine(
+                                       "TestDDAComplete",
+                                       "Directory=" + tempFile.getParent(),
+                                       "ReadDirectoryAllowed=true",
+                                       "EndMessage"
+                               );
+                       }
+
+                       private Matcher<List<String>> matchesTestDDAResponse(File tempFile) {
+                               return matchesFcpMessage(
+                                       "TestDDAResponse",
+                                       "Directory=" + tempFile.getParent(),
+                                       "ReadContent=test-content"
+                               );
+                       }
+
+                       private void sendTestDDAReply(String directory, File tempFile) throws IOException {
+                               fcpServer.writeLine(
+                                       "TestDDAReply",
+                                       "Directory=" + directory,
+                                       "ReadFilename=" + tempFile,
+                                       "EndMessage"
+                               );
+                       }
+
+                       private Matcher<List<String>> matchesTestDDARequest(File tempFile) {
+                               return matchesFcpMessage(
+                                       "TestDDARequest",
+                                       "Directory=" + tempFile.getParent(),
+                                       "WantReadDirectory=true",
+                                       "WantWriteDirectory=false"
+                               );
+                       }
+
+                       private void sendDdaRequired(String identifier) throws IOException {
+                               fcpServer.writeLine(
+                                       "ProtocolError",
+                                       "Identifier=" + identifier,
+                                       "Code=25",
+                                       "EndMessage"
+                               );
+                       }
+
+               }
+
+               private void replyWithPutSuccessful(String identifier) throws IOException {
+                       fcpServer.writeLine(
+                               "PutSuccessful",
+                               "URI=KSK@foo.txt",
+                               "Identifier=" + identifier,
+                               "EndMessage"
+                       );
+               }
+
+               private void replyWithPutFailed(String identifier) throws IOException {
+                       fcpServer.writeLine(
+                               "PutFailed",
+                               "Identifier=" + identifier,
+                               "EndMessage"
+                       );
+               }
+
+               private Matcher<List<String>> matchesDirectClientPut(String... additionalLines) {
+                       List<String> lines = new ArrayList<>(Arrays.asList("UploadFrom=direct", "DataLength=6", "URI=KSK@foo.txt"));
+                       Arrays.asList(additionalLines).forEach(lines::add);
+                       return allOf(
+                               hasHead("ClientPut"),
+                               hasParameters(1, 2, lines.toArray(new String[lines.size()])),
+                               hasTail("EndMessage", "Hello")
+                       );
+               }
+
+               private File createDdaFile() throws IOException {
+                       File tempFile = File.createTempFile("test-dda-", ".dat");
+                       tempFile.deleteOnExit();
+                       Files.write("test-content", tempFile, StandardCharsets.UTF_8);
+                       return tempFile;
+               }
+
+               @Test
+               public void clientPutDoesNotReactToProtocolErrorForDifferentIdentifier()
+               throws InterruptedException, ExecutionException, IOException {
+                       Future<Optional<Key>> key = fcpClient.clientPut().from(new File("/tmp/data.txt")).uri("KSK@foo.txt").execute();
+                       connectNode();
+                       List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+                       String identifier = extractIdentifier(lines);
+                       fcpServer.writeLine(
+                               "ProtocolError",
+                               "Identifier=not-the-right-one",
+                               "Code=25",
+                               "EndMessage"
+                       );
+                       fcpServer.writeLine(
+                               "PutSuccessful",
+                               "Identifier=" + identifier,
+                               "URI=KSK@foo.txt",
+                               "EndMessage"
+                       );
+                       assertThat(key.get().get().getKey(), is("KSK@foo.txt"));
+               }
+
+               @Test
+               public void clientPutAbortsOnProtocolErrorOtherThan25()
+               throws InterruptedException, ExecutionException, IOException {
+                       Future<Optional<Key>> key = fcpClient.clientPut().from(new File("/tmp/data.txt")).uri("KSK@foo.txt").execute();
+                       connectNode();
+                       List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+                       String identifier = extractIdentifier(lines);
+                       fcpServer.writeLine(
+                               "ProtocolError",
+                               "Identifier=" + identifier,
+                               "Code=1",
+                               "EndMessage"
+                       );
+                       assertThat(key.get().isPresent(), is(false));
+               }
+
+               @Test
+               public void clientPutSendsNotificationsForGeneratedKeys()
+               throws InterruptedException, ExecutionException, IOException {
+                       List<String> generatedKeys = new CopyOnWriteArrayList<>();
+                       Future<Optional<Key>> key = fcpClient.clientPut()
+                               .onKeyGenerated(generatedKeys::add)
+                               .from(new ByteArrayInputStream("Hello\n".getBytes()))
+                               .length(6)
+                               .uri("KSK@foo.txt")
+                               .execute();
+                       connectNode();
+                       List<String> lines = fcpServer.collectUntil(is("Hello"));
+                       String identifier = extractIdentifier(lines);
+                       fcpServer.writeLine(
+                               "URIGenerated",
+                               "Identifier=" + identifier,
+                               "URI=KSK@foo.txt",
+                               "EndMessage"
+                       );
+                       replyWithPutSuccessful(identifier);
+                       assertThat(key.get().get().getKey(), is("KSK@foo.txt"));
+                       assertThat(generatedKeys, contains("KSK@foo.txt"));
+               }
+
+               @Test
+               public void clientPutSendsNotificationOnProgress() throws InterruptedException, ExecutionException, IOException {
+                       List<RequestProgress> requestProgress = new ArrayList<>();
+                   Future<Optional<Key>> key = fcpClient.clientPut()
+                               .onProgress(requestProgress::add)
+                               .from(new ByteArrayInputStream("Hello\n".getBytes()))
+                               .length(6)
+                               .uri("KSK@foo.txt")
+                               .execute();
+                       connectNode();
+                       readMessage("Hello", () -> matchesDirectClientPut("Verbosity=1"));
+                       replyWithSimpleProgress(1, 2, 3, 4, 5, 6, true, 8);
+                       replyWithSimpleProgress(11, 12, 13, 14, 15, 16, false, 18);
+                       replyWithPutSuccessful(identifier);
+                       assertThat(key.get().get().getKey(), is("KSK@foo.txt"));
+                       assertThat(requestProgress, contains(
+                               isRequestProgress(1, 2, 3, 4, 5, 6, true, 8),
+                               isRequestProgress(11, 12, 13, 14, 15, 16, false, 18)
+                       ));
+               }
+
+               private void replyWithSimpleProgress(
+                       int total, int required, int failed, int fatallyFailed, int succeeded, int lastProgress,
+                       boolean finalizedTotal, int minSuccessFetchBlocks) throws IOException {
+                       fcpServer.writeLine(
+                               "SimpleProgress",
+                               "Identifier=" + identifier,
+                               "Total=" + total,
+                               "Required=" + required,
+                               "Failed=" + failed,
+                               "FatallyFailed=" + fatallyFailed,
+                               "Succeeded=" + succeeded,
+                               "LastProgress=" + lastProgress,
+                               "FinalizedTotal=" + finalizedTotal,
+                               "MinSuccessFetchBlocks=" + minSuccessFetchBlocks,
+                               "EndMessage"
+                       );
+               }
+
+       }
+
+       public class ClientPutDiskDir {
+
+               @Test
+               public void commandIsSentCorrectly() throws InterruptedException, ExecutionException, IOException {
+                       Future<Optional<Key>> key = fcpClient.clientPutDiskDir().fromDirectory(new File("")).uri("CHK@").execute();
+                       connectAndAssert(this::matchesClientPutDiskDir);
+                       fcpServer.writeLine("PutSuccessful", "Identifier=" + identifier, "URI=CHK@abc", "EndMessage");
+                       assertThat(key.get().get().getKey(), is("CHK@abc"));
+               }
+
+               @Test
+               public void protocolErrorAbortsCommand() throws InterruptedException, ExecutionException, IOException {
+                       Future<Optional<Key>> key = fcpClient.clientPutDiskDir().fromDirectory(new File("")).uri("CHK@").execute();
+                       connectAndAssert(this::matchesClientPutDiskDir);
+                       replyWithProtocolError();
+                       assertThat(key.get().isPresent(), is(false));
+               }
+
+               private Matcher<List<String>> matchesClientPutDiskDir() {
+                       return matchesFcpMessage(
+                               "ClientPutDiskDir",
+                               "Identifier=" + identifier,
+                               "URI=CHK@",
+                               "Filename=" + new File("").getPath()
+                       );
+               }
+
+       }
+
+       public class ConfigCommand {
+
+               public class GetConfig {
+
+                       @Test
+                       public void defaultFcpClientCanGetConfigWithoutDetails()
+                       throws InterruptedException, ExecutionException, IOException {
+                               Future<ConfigData> configData = fcpClient.getConfig().execute();
+                               connectAndAssert(() -> matchesFcpMessage("GetConfig", "Identifier=" + identifier));
+                               replyWithConfigData();
+                               assertThat(configData.get(), notNullValue());
+                       }
+
+                       @Test
+                       public void defaultFcpClientCanGetConfigWithCurrent()
+                       throws InterruptedException, ExecutionException, IOException {
+                               Future<ConfigData> configData = fcpClient.getConfig().withCurrent().execute();
+                               connectAndAssert(() -> matchesGetConfigWithAdditionalParameter("WithCurrent"));
+                               replyWithConfigData("current.foo=bar");
+                               assertThat(configData.get().getCurrent("foo"), is("bar"));
+                       }
+
+                       @Test
+                       public void defaultFcpClientCanGetConfigWithDefaults()
+                       throws InterruptedException, ExecutionException, IOException {
+                               Future<ConfigData> configData = fcpClient.getConfig().withDefaults().execute();
+                               connectAndAssert(() -> matchesGetConfigWithAdditionalParameter("WithDefaults"));
+                               replyWithConfigData("default.foo=bar");
+                               assertThat(configData.get().getDefault("foo"), is("bar"));
+                       }
+
+                       @Test
+                       public void defaultFcpClientCanGetConfigWithSortOrder()
+                       throws InterruptedException, ExecutionException, IOException {
+                               Future<ConfigData> configData = fcpClient.getConfig().withSortOrder().execute();
+                               connectAndAssert(() -> matchesGetConfigWithAdditionalParameter("WithSortOrder"));
+                               replyWithConfigData("sortOrder.foo=17");
+                               assertThat(configData.get().getSortOrder("foo"), is(17));
+                       }
+
+                       @Test
+                       public void defaultFcpClientCanGetConfigWithExpertFlag()
+                       throws InterruptedException, ExecutionException, IOException {
+                               Future<ConfigData> configData = fcpClient.getConfig().withExpertFlag().execute();
+                               connectAndAssert(() -> matchesGetConfigWithAdditionalParameter("WithExpertFlag"));
+                               replyWithConfigData("expertFlag.foo=true");
+                               assertThat(configData.get().getExpertFlag("foo"), is(true));
+                       }
+
+                       @Test
+                       public void defaultFcpClientCanGetConfigWithForceWriteFlag()
+                       throws InterruptedException, ExecutionException, IOException {
+                               Future<ConfigData> configData = fcpClient.getConfig().withForceWriteFlag().execute();
+                               connectAndAssert(() -> matchesGetConfigWithAdditionalParameter("WithForceWriteFlag"));
+                               replyWithConfigData("forceWriteFlag.foo=true");
+                               assertThat(configData.get().getForceWriteFlag("foo"), is(true));
+                       }
+
+                       @Test
+                       public void defaultFcpClientCanGetConfigWithShortDescription()
+                       throws InterruptedException, ExecutionException, IOException {
+                               Future<ConfigData> configData = fcpClient.getConfig().withShortDescription().execute();
+                               connectAndAssert(() -> matchesGetConfigWithAdditionalParameter("WithShortDescription"));
+                               replyWithConfigData("shortDescription.foo=bar");
+                               assertThat(configData.get().getShortDescription("foo"), is("bar"));
+                       }
+
+                       @Test
+                       public void defaultFcpClientCanGetConfigWithLongDescription()
+                       throws InterruptedException, ExecutionException, IOException {
+                               Future<ConfigData> configData = fcpClient.getConfig().withLongDescription().execute();
+                               connectAndAssert(() -> matchesGetConfigWithAdditionalParameter("WithLongDescription"));
+                               replyWithConfigData("longDescription.foo=bar");
+                               assertThat(configData.get().getLongDescription("foo"), is("bar"));
+                       }
+
+                       @Test
+                       public void defaultFcpClientCanGetConfigWithDataTypes()
+                       throws InterruptedException, ExecutionException, IOException {
+                               Future<ConfigData> configData = fcpClient.getConfig().withDataTypes().execute();
+                               connectAndAssert(() -> matchesGetConfigWithAdditionalParameter("WithDataTypes"));
+                               replyWithConfigData("dataType.foo=number");
+                               assertThat(configData.get().getDataType("foo"), is("number"));
+                       }
+
+                       private Matcher<List<String>> matchesGetConfigWithAdditionalParameter(String additionalParameter) {
+                               return matchesFcpMessage(
+                                       "GetConfig",
+                                       "Identifier=" + identifier,
+                                       additionalParameter + "=true"
+                               );
+                       }
+
+               }
+
+               public class ModifyConfig {
+
+                       @Test
+                       public void defaultFcpClientCanModifyConfigData()
+                       throws InterruptedException, ExecutionException, IOException {
+                               Future<ConfigData> newConfigData = fcpClient.modifyConfig().set("foo.bar").to("baz").execute();
+                               connectAndAssert(() -> matchesFcpMessage(
+                                       "ModifyConfig",
+                                       "Identifier=" + identifier,
+                                       "foo.bar=baz"
+                               ));
+                               replyWithConfigData("current.foo.bar=baz");
+                               assertThat(newConfigData.get().getCurrent("foo.bar"), is("baz"));
+                       }
+
+               }
+
+               private void replyWithConfigData(String... additionalLines) throws IOException {
+                       fcpServer.writeLine("ConfigData", "Identifier=" + identifier);
+                       fcpServer.writeLine(additionalLines);
+                       fcpServer.writeLine("EndMessage");
+               }
+
+       }
+
+       public class NodeInformation {
+
+               @Test
+               public void defaultFcpClientCanGetNodeInformation() throws InterruptedException, ExecutionException, IOException {
+                       Future<NodeData> nodeData = fcpClient.getNode().execute();
+                       connectAndAssert(() -> matchesGetNode(false, false, false));
+                       replyWithNodeData();
+                       assertThat(nodeData.get(), notNullValue());
+                       assertThat(nodeData.get().getNodeRef().isOpennet(), is(false));
+               }
+
+               @Test
+               public void defaultFcpClientCanGetNodeInformationWithOpennetRef()
+               throws InterruptedException, ExecutionException, IOException {
+                       Future<NodeData> nodeData = fcpClient.getNode().opennetRef().execute();
+                       connectAndAssert(() -> matchesGetNode(true, false, false));
+                       replyWithNodeData("opennet=true");
+                       assertThat(nodeData.get().getVersion().toString(), is("Fred,0.7,1.0,1466"));
+                       assertThat(nodeData.get().getNodeRef().isOpennet(), is(true));
+               }
+
+               @Test
+               public void defaultFcpClientCanGetNodeInformationWithPrivateData()
+               throws InterruptedException, ExecutionException, IOException {
+                       Future<NodeData> nodeData = fcpClient.getNode().includePrivate().execute();
+                       connectAndAssert(() -> matchesGetNode(false, true, false));
+                       replyWithNodeData("ark.privURI=SSK@XdHMiRl");
+                       assertThat(nodeData.get().getARK().getPrivateURI(), is("SSK@XdHMiRl"));
+               }
+
+               @Test
+               public void defaultFcpClientCanGetNodeInformationWithVolatileData()
+               throws InterruptedException, ExecutionException, IOException {
+                       Future<NodeData> nodeData = fcpClient.getNode().includeVolatile().execute();
+                       connectAndAssert(() -> matchesGetNode(false, false, true));
+                       replyWithNodeData("volatile.freeJavaMemory=205706528");
+                       assertThat(nodeData.get().getVolatile("freeJavaMemory"), is("205706528"));
+               }
+
+               private Matcher<List<String>> matchesGetNode(boolean withOpennetRef, boolean withPrivate, boolean withVolatile) {
+                       return matchesFcpMessage(
+                               "GetNode",
+                               "Identifier=" + identifier,
+                               "GiveOpennetRef=" + withOpennetRef,
+                               "WithPrivate=" + withPrivate,
+                               "WithVolatile=" + withVolatile
+                       );
+               }
+
+               private void replyWithNodeData(String... additionalLines) throws IOException {
+                       fcpServer.writeLine(
+                               "NodeData",
+                               "Identifier=" + identifier,
+                               "ark.pubURI=SSK@3YEf.../ark",
+                               "ark.number=78",
+                               "auth.negTypes=2",
+                               "version=Fred,0.7,1.0,1466",
+                               "lastGoodVersion=Fred,0.7,1.0,1466"
+                       );
+                       fcpServer.writeLine(additionalLines);
+                       fcpServer.writeLine("EndMessage");
+               }
+
+       }
+
 }