Refactor connection tests a bit more
[jFCPlib.git] / src / test / java / net / pterodactylus / fcp / quelaton / DefaultFcpClientTest.java
1 package net.pterodactylus.fcp.quelaton;
2
3 import static org.hamcrest.MatcherAssert.assertThat;
4 import static org.hamcrest.Matchers.allOf;
5 import static org.hamcrest.Matchers.contains;
6 import static org.hamcrest.Matchers.containsInAnyOrder;
7 import static org.hamcrest.Matchers.hasItem;
8 import static org.hamcrest.Matchers.hasSize;
9 import static org.hamcrest.Matchers.is;
10 import static org.hamcrest.Matchers.not;
11 import static org.hamcrest.Matchers.notNullValue;
12 import static org.hamcrest.Matchers.startsWith;
13
14 import java.io.ByteArrayInputStream;
15 import java.io.File;
16 import java.io.IOException;
17 import java.net.URL;
18 import java.nio.charset.StandardCharsets;
19 import java.util.Arrays;
20 import java.util.Collection;
21 import java.util.List;
22 import java.util.Optional;
23 import java.util.concurrent.CopyOnWriteArrayList;
24 import java.util.concurrent.CountDownLatch;
25 import java.util.concurrent.ExecutionException;
26 import java.util.concurrent.ExecutorService;
27 import java.util.concurrent.Executors;
28 import java.util.concurrent.Future;
29 import java.util.concurrent.TimeUnit;
30 import java.util.concurrent.atomic.AtomicBoolean;
31 import java.util.concurrent.atomic.AtomicInteger;
32 import java.util.function.Supplier;
33 import java.util.stream.Collectors;
34
35 import net.pterodactylus.fcp.ARK;
36 import net.pterodactylus.fcp.ConfigData;
37 import net.pterodactylus.fcp.DSAGroup;
38 import net.pterodactylus.fcp.FcpKeyPair;
39 import net.pterodactylus.fcp.Key;
40 import net.pterodactylus.fcp.NodeData;
41 import net.pterodactylus.fcp.NodeRef;
42 import net.pterodactylus.fcp.Peer;
43 import net.pterodactylus.fcp.PeerNote;
44 import net.pterodactylus.fcp.PluginInfo;
45 import net.pterodactylus.fcp.Priority;
46 import net.pterodactylus.fcp.fake.FakeTcpServer;
47 import net.pterodactylus.fcp.quelaton.ClientGetCommand.Data;
48
49 import com.google.common.io.ByteStreams;
50 import com.google.common.io.Files;
51 import com.nitorcreations.junit.runners.NestedRunner;
52 import org.hamcrest.Description;
53 import org.hamcrest.Matcher;
54 import org.hamcrest.Matchers;
55 import org.hamcrest.TypeSafeDiagnosingMatcher;
56 import org.junit.After;
57 import org.junit.Assert;
58 import org.junit.Test;
59 import org.junit.runner.RunWith;
60
61 /**
62  * Unit test for {@link DefaultFcpClient}.
63  *
64  * @author <a href="bombe@freenetproject.org">David ‘Bombe’ Roden</a>
65  */
66 @RunWith(NestedRunner.class)
67 public class DefaultFcpClientTest {
68
69         private static final String INSERT_URI =
70                 "SSK@RVCHbJdkkyTCeNN9AYukEg76eyqmiosSaNKgE3U9zUw,7SHH53gletBVb9JD7nBsyClbLQsBubDPEIcwg908r7Y,AQECAAE/";
71         private static final String REQUEST_URI =
72                 "SSK@wtbgd2loNcJCXvtQVOftl2tuWBomDQHfqS6ytpPRhfw,7SHH53gletBVb9JD7nBsyClbLQsBubDPEIcwg908r7Y,AQACAAE/";
73
74         private int threadCounter = 0;
75         private final ExecutorService threadPool =
76                 Executors.newCachedThreadPool(r -> new Thread(r, "Test-Thread-" + threadCounter++));
77         private final FakeTcpServer fcpServer;
78         private final DefaultFcpClient fcpClient;
79
80         public DefaultFcpClientTest() throws IOException {
81                 fcpServer = new FakeTcpServer(threadPool);
82                 fcpClient = new DefaultFcpClient(threadPool, "localhost", fcpServer.getPort(), () -> "Test");
83         }
84
85         @After
86         public void tearDown() throws IOException {
87                 fcpServer.close();
88                 threadPool.shutdown();
89         }
90
91         private void connectNode() throws InterruptedException, ExecutionException, IOException {
92                 fcpServer.connect().get();
93                 fcpServer.collectUntil(is("EndMessage"));
94                 fcpServer.writeLine("NodeHello",
95                         "CompressionCodecs=4 - GZIP(0), BZIP2(1), LZMA(2), LZMA_NEW(3)",
96                         "Revision=build01466",
97                         "Testnet=false",
98                         "Version=Fred,0.7,1.0,1466",
99                         "Build=1466",
100                         "ConnectionIdentifier=14318898267048452a81b36e7f13a3f0",
101                         "Node=Fred",
102                         "ExtBuild=29",
103                         "FCPVersion=2.0",
104                         "NodeLanguage=ENGLISH",
105                         "ExtRevision=v29",
106                         "EndMessage"
107                 );
108         }
109
110         private String extractIdentifier(List<String> lines) {
111                 return lines.stream()
112                         .filter(s -> s.startsWith("Identifier="))
113                         .map(s -> s.substring(s.indexOf('=') + 1))
114                         .findFirst()
115                         .orElse("");
116         }
117
118         private Matcher<List<String>> matchesFcpMessage(String name, String... requiredLines) {
119                 return matchesFcpMessageWithTerminator(name, "EndMessage", requiredLines);
120         }
121
122         private Matcher<Iterable<String>> hasHead(String firstElement) {
123                 return new TypeSafeDiagnosingMatcher<Iterable<String>>() {
124                         @Override
125                         protected boolean matchesSafely(Iterable<String> iterable, Description mismatchDescription) {
126                                 if (!iterable.iterator().hasNext()) {
127                                         mismatchDescription.appendText("is empty");
128                                         return false;
129                                 }
130                                 String element = iterable.iterator().next();
131                                 if (!element.equals(firstElement)) {
132                                         mismatchDescription.appendText("starts with ").appendValue(element);
133                                         return false;
134                                 }
135                                 return true;
136                         }
137
138                         @Override
139                         public void describeTo(Description description) {
140                                 description.appendText("starts with ").appendValue(firstElement);
141                         }
142                 };
143         }
144
145         private Matcher<List<String>> matchesFcpMessageWithTerminator(
146                 String name, String terminator, String... requiredLines) {
147                 return allOf(hasHead(name), hasParameters(1, 1, requiredLines), hasTail(terminator));
148         }
149
150         private Matcher<List<String>> hasParameters(int ignoreStart, int ignoreEnd, String... lines) {
151                 return new TypeSafeDiagnosingMatcher<List<String>>() {
152                         @Override
153                         protected boolean matchesSafely(List<String> item, Description mismatchDescription) {
154                                 if (item.size() < (ignoreStart + ignoreEnd)) {
155                                         mismatchDescription.appendText("has only ").appendValue(item.size()).appendText(" elements");
156                                         return false;
157                                 }
158                                 for (String line : lines) {
159                                         if ((item.indexOf(line) < ignoreStart) || (item.indexOf(line) >= (item.size() - ignoreEnd))) {
160                                                 mismatchDescription.appendText("does not contains ").appendValue(line);
161                                                 return false;
162                                         }
163                                 }
164                                 return true;
165                         }
166
167                         @Override
168                         public void describeTo(Description description) {
169                                 description.appendText("contains ").appendValueList("(", ", ", ")", lines);
170                                 description.appendText(", ignoring the first ").appendValue(ignoreStart);
171                                 description.appendText(" and the last ").appendValue(ignoreEnd);
172                         }
173                 };
174         }
175
176         private Matcher<List<String>> hasTail(String... lastElements) {
177                 return new TypeSafeDiagnosingMatcher<List<String>>() {
178                         @Override
179                         protected boolean matchesSafely(List<String> list, Description mismatchDescription) {
180                                 if (list.size() < lastElements.length) {
181                                         mismatchDescription.appendText("is too small");
182                                         return false;
183                                 }
184                                 List<String> tail = list.subList(list.size() - lastElements.length, list.size());
185                                 if (!tail.equals(Arrays.asList(lastElements))) {
186                                         mismatchDescription.appendText("ends with ").appendValueList("(", ", ", ")", tail);
187                                         return false;
188                                 }
189                                 return true;
190                         }
191
192                         @Override
193                         public void describeTo(Description description) {
194                                 description.appendText("ends with ").appendValueList("(", ", ", ")", lastElements);
195                         }
196                 };
197         }
198
199         @Test
200         public void clientPutWithDirectDataSendsCorrectCommand()
201         throws IOException, ExecutionException, InterruptedException {
202                 fcpClient.clientPut()
203                         .from(new ByteArrayInputStream("Hello\n".getBytes()))
204                         .length(6)
205                         .uri("KSK@foo.txt")
206                         .execute();
207                 connectNode();
208                 List<String> lines = fcpServer.collectUntil(is("Hello"));
209                 assertThat(lines, allOf(
210                         hasHead("ClientPut"),
211                         hasParameters(1, 2, "UploadFrom=direct", "DataLength=6", "URI=KSK@foo.txt"),
212                         hasTail("EndMessage", "Hello")
213                 ));
214         }
215
216         @Test
217         public void clientPutWithDirectDataSucceedsOnCorrectIdentifier()
218         throws InterruptedException, ExecutionException, IOException {
219                 Future<Optional<Key>> key = fcpClient.clientPut()
220                         .from(new ByteArrayInputStream("Hello\n".getBytes()))
221                         .length(6)
222                         .uri("KSK@foo.txt")
223                         .execute();
224                 connectNode();
225                 List<String> lines = fcpServer.collectUntil(is("Hello"));
226                 String identifier = extractIdentifier(lines);
227                 fcpServer.writeLine(
228                         "PutFailed",
229                         "Identifier=not-the-right-one",
230                         "EndMessage"
231                 );
232                 fcpServer.writeLine(
233                         "PutSuccessful",
234                         "URI=KSK@foo.txt",
235                         "Identifier=" + identifier,
236                         "EndMessage"
237                 );
238                 assertThat(key.get().get().getKey(), is("KSK@foo.txt"));
239         }
240
241         @Test
242         public void clientPutWithDirectDataFailsOnCorrectIdentifier()
243         throws InterruptedException, ExecutionException, IOException {
244                 Future<Optional<Key>> key = fcpClient.clientPut()
245                         .from(new ByteArrayInputStream("Hello\n".getBytes()))
246                         .length(6)
247                         .uri("KSK@foo.txt")
248                         .execute();
249                 connectNode();
250                 List<String> lines = fcpServer.collectUntil(is("Hello"));
251                 String identifier = extractIdentifier(lines);
252                 fcpServer.writeLine(
253                         "PutSuccessful",
254                         "Identifier=not-the-right-one",
255                         "URI=KSK@foo.txt",
256                         "EndMessage"
257                 );
258                 fcpServer.writeLine(
259                         "PutFailed",
260                         "Identifier=" + identifier,
261                         "EndMessage"
262                 );
263                 assertThat(key.get().isPresent(), is(false));
264         }
265
266         @Test
267         public void clientPutWithRenamedDirectDataSendsCorrectCommand()
268         throws InterruptedException, ExecutionException, IOException {
269                 fcpClient.clientPut()
270                         .named("otherName.txt")
271                         .from(new ByteArrayInputStream("Hello\n".getBytes()))
272                         .length(6)
273                         .uri("KSK@foo.txt")
274                         .execute();
275                 connectNode();
276                 List<String> lines = fcpServer.collectUntil(is("Hello"));
277                 assertThat(lines, allOf(
278                         hasHead("ClientPut"),
279                         hasParameters(1, 2, "TargetFilename=otherName.txt", "UploadFrom=direct", "DataLength=6", "URI=KSK@foo.txt"),
280                         hasTail("EndMessage", "Hello")
281                 ));
282         }
283
284         @Test
285         public void clientPutWithRedirectSendsCorrectCommand()
286         throws IOException, ExecutionException, InterruptedException {
287                 fcpClient.clientPut().redirectTo("KSK@bar.txt").uri("KSK@foo.txt").execute();
288                 connectNode();
289                 List<String> lines = fcpServer.collectUntil(is("EndMessage"));
290                 assertThat(lines,
291                         matchesFcpMessage("ClientPut", "UploadFrom=redirect", "URI=KSK@foo.txt", "TargetURI=KSK@bar.txt"));
292         }
293
294         @Test
295         public void clientPutWithFileSendsCorrectCommand() throws InterruptedException, ExecutionException, IOException {
296                 fcpClient.clientPut().from(new File("/tmp/data.txt")).uri("KSK@foo.txt").execute();
297                 connectNode();
298                 List<String> lines = fcpServer.collectUntil(is("EndMessage"));
299                 assertThat(lines,
300                         matchesFcpMessage("ClientPut", "UploadFrom=disk", "URI=KSK@foo.txt", "Filename=/tmp/data.txt"));
301         }
302
303         @Test
304         public void clientPutWithFileCanCompleteTestDdaSequence()
305         throws IOException, ExecutionException, InterruptedException {
306                 File tempFile = createTempFile();
307                 fcpClient.clientPut().from(new File(tempFile.getParent(), "test.dat")).uri("KSK@foo.txt").execute();
308                 connectNode();
309                 List<String> lines = fcpServer.collectUntil(is("EndMessage"));
310                 String identifier = extractIdentifier(lines);
311                 fcpServer.writeLine(
312                         "ProtocolError",
313                         "Identifier=" + identifier,
314                         "Code=25",
315                         "EndMessage"
316                 );
317                 lines = fcpServer.collectUntil(is("EndMessage"));
318                 assertThat(lines, matchesFcpMessage(
319                         "TestDDARequest",
320                         "Directory=" + tempFile.getParent(),
321                         "WantReadDirectory=true",
322                         "WantWriteDirectory=false"
323                 ));
324                 fcpServer.writeLine(
325                         "TestDDAReply",
326                         "Directory=" + tempFile.getParent(),
327                         "ReadFilename=" + tempFile,
328                         "EndMessage"
329                 );
330                 lines = fcpServer.collectUntil(is("EndMessage"));
331                 assertThat(lines, matchesFcpMessage(
332                         "TestDDAResponse",
333                         "Directory=" + tempFile.getParent(),
334                         "ReadContent=test-content"
335                 ));
336                 fcpServer.writeLine(
337                         "TestDDAComplete",
338                         "Directory=" + tempFile.getParent(),
339                         "ReadDirectoryAllowed=true",
340                         "EndMessage"
341                 );
342                 lines = fcpServer.collectUntil(is("EndMessage"));
343                 assertThat(lines,
344                         matchesFcpMessage("ClientPut", "UploadFrom=disk", "URI=KSK@foo.txt",
345                                 "Filename=" + new File(tempFile.getParent(), "test.dat")));
346         }
347
348         private File createTempFile() throws IOException {
349                 File tempFile = File.createTempFile("test-dda-", ".dat");
350                 tempFile.deleteOnExit();
351                 Files.write("test-content", tempFile, StandardCharsets.UTF_8);
352                 return tempFile;
353         }
354
355         @Test
356         public void clientPutDoesNotReactToProtocolErrorForDifferentIdentifier()
357         throws InterruptedException, ExecutionException, IOException {
358                 Future<Optional<Key>> key = fcpClient.clientPut().from(new File("/tmp/data.txt")).uri("KSK@foo.txt").execute();
359                 connectNode();
360                 List<String> lines = fcpServer.collectUntil(is("EndMessage"));
361                 String identifier = extractIdentifier(lines);
362                 fcpServer.writeLine(
363                         "ProtocolError",
364                         "Identifier=not-the-right-one",
365                         "Code=25",
366                         "EndMessage"
367                 );
368                 fcpServer.writeLine(
369                         "PutSuccessful",
370                         "Identifier=" + identifier,
371                         "URI=KSK@foo.txt",
372                         "EndMessage"
373                 );
374                 assertThat(key.get().get().getKey(), is("KSK@foo.txt"));
375         }
376
377         @Test
378         public void clientPutAbortsOnProtocolErrorOtherThan25()
379         throws InterruptedException, ExecutionException, IOException {
380                 Future<Optional<Key>> key = fcpClient.clientPut().from(new File("/tmp/data.txt")).uri("KSK@foo.txt").execute();
381                 connectNode();
382                 List<String> lines = fcpServer.collectUntil(is("EndMessage"));
383                 String identifier = extractIdentifier(lines);
384                 fcpServer.writeLine(
385                         "ProtocolError",
386                         "Identifier=" + identifier,
387                         "Code=1",
388                         "EndMessage"
389                 );
390                 assertThat(key.get().isPresent(), is(false));
391         }
392
393         @Test
394         public void clientPutDoesNotReplyToWrongTestDdaReply() throws IOException, ExecutionException,
395         InterruptedException {
396                 File tempFile = createTempFile();
397                 fcpClient.clientPut().from(new File(tempFile.getParent(), "test.dat")).uri("KSK@foo.txt").execute();
398                 connectNode();
399                 List<String> lines = fcpServer.collectUntil(is("EndMessage"));
400                 String identifier = extractIdentifier(lines);
401                 fcpServer.writeLine(
402                         "ProtocolError",
403                         "Identifier=" + identifier,
404                         "Code=25",
405                         "EndMessage"
406                 );
407                 lines = fcpServer.collectUntil(is("EndMessage"));
408                 assertThat(lines, matchesFcpMessage(
409                         "TestDDARequest",
410                         "Directory=" + tempFile.getParent(),
411                         "WantReadDirectory=true",
412                         "WantWriteDirectory=false"
413                 ));
414                 fcpServer.writeLine(
415                         "TestDDAReply",
416                         "Directory=/some-other-directory",
417                         "ReadFilename=" + tempFile,
418                         "EndMessage"
419                 );
420                 fcpServer.writeLine(
421                         "TestDDAReply",
422                         "Directory=" + tempFile.getParent(),
423                         "ReadFilename=" + tempFile,
424                         "EndMessage"
425                 );
426                 lines = fcpServer.collectUntil(is("EndMessage"));
427                 assertThat(lines, matchesFcpMessage(
428                         "TestDDAResponse",
429                         "Directory=" + tempFile.getParent(),
430                         "ReadContent=test-content"
431                 ));
432         }
433
434         @Test
435         public void clientPutSendsResponseEvenIfFileCanNotBeRead()
436         throws IOException, ExecutionException, InterruptedException {
437                 File tempFile = createTempFile();
438                 fcpClient.clientPut().from(new File(tempFile.getParent(), "test.dat")).uri("KSK@foo.txt").execute();
439                 connectNode();
440                 List<String> lines = fcpServer.collectUntil(is("EndMessage"));
441                 String identifier = extractIdentifier(lines);
442                 fcpServer.writeLine(
443                         "ProtocolError",
444                         "Identifier=" + identifier,
445                         "Code=25",
446                         "EndMessage"
447                 );
448                 lines = fcpServer.collectUntil(is("EndMessage"));
449                 assertThat(lines, matchesFcpMessage(
450                         "TestDDARequest",
451                         "Directory=" + tempFile.getParent(),
452                         "WantReadDirectory=true",
453                         "WantWriteDirectory=false"
454                 ));
455                 fcpServer.writeLine(
456                         "TestDDAReply",
457                         "Directory=" + tempFile.getParent(),
458                         "ReadFilename=" + tempFile + ".foo",
459                         "EndMessage"
460                 );
461                 lines = fcpServer.collectUntil(is("EndMessage"));
462                 assertThat(lines, matchesFcpMessage(
463                         "TestDDAResponse",
464                         "Directory=" + tempFile.getParent(),
465                         "ReadContent=failed-to-read"
466                 ));
467         }
468
469         @Test
470         public void clientPutDoesNotResendOriginalClientPutOnTestDDACompleteWithWrongDirectory()
471         throws IOException, ExecutionException, InterruptedException {
472                 File tempFile = createTempFile();
473                 fcpClient.clientPut().from(new File(tempFile.getParent(), "test.dat")).uri("KSK@foo.txt").execute();
474                 connectNode();
475                 List<String> lines = fcpServer.collectUntil(is("EndMessage"));
476                 String identifier = extractIdentifier(lines);
477                 fcpServer.writeLine(
478                         "TestDDAComplete",
479                         "Directory=/some-other-directory",
480                         "EndMessage"
481                 );
482                 fcpServer.writeLine(
483                         "ProtocolError",
484                         "Identifier=" + identifier,
485                         "Code=25",
486                         "EndMessage"
487                 );
488                 lines = fcpServer.collectUntil(is("EndMessage"));
489                 assertThat(lines, matchesFcpMessage(
490                         "TestDDARequest",
491                         "Directory=" + tempFile.getParent(),
492                         "WantReadDirectory=true",
493                         "WantWriteDirectory=false"
494                 ));
495         }
496
497         @Test
498         public void clientPutSendsNotificationsForGeneratedKeys()
499         throws InterruptedException, ExecutionException, IOException {
500                 List<String> generatedKeys = new CopyOnWriteArrayList<>();
501                 Future<Optional<Key>> key = fcpClient.clientPut()
502                         .onKeyGenerated(generatedKeys::add)
503                         .from(new ByteArrayInputStream("Hello\n".getBytes()))
504                         .length(6)
505                         .uri("KSK@foo.txt")
506                         .execute();
507                 connectNode();
508                 List<String> lines = fcpServer.collectUntil(is("Hello"));
509                 String identifier = extractIdentifier(lines);
510                 fcpServer.writeLine(
511                         "URIGenerated",
512                         "Identifier=" + identifier,
513                         "URI=KSK@foo.txt",
514                         "EndMessage"
515                 );
516                 fcpServer.writeLine(
517                         "PutSuccessful",
518                         "URI=KSK@foo.txt",
519                         "Identifier=" + identifier,
520                         "EndMessage"
521                 );
522                 assertThat(key.get().get().getKey(), is("KSK@foo.txt"));
523                 assertThat(generatedKeys, contains("KSK@foo.txt"));
524         }
525
526         @Test
527         public void defaultFcpClientCanGetNodeInformation() throws InterruptedException, ExecutionException, IOException {
528                 Future<NodeData> nodeData = fcpClient.getNode().execute();
529                 connectNode();
530                 List<String> lines = fcpServer.collectUntil(is("EndMessage"));
531                 String identifier = extractIdentifier(lines);
532                 assertThat(lines, matchesFcpMessage(
533                         "GetNode",
534                         "Identifier=" + identifier,
535                         "GiveOpennetRef=false",
536                         "WithPrivate=false",
537                         "WithVolatile=false"
538                 ));
539                 fcpServer.writeLine(
540                         "NodeData",
541                         "Identifier=" + identifier,
542                         "ark.pubURI=SSK@3YEf.../ark",
543                         "ark.number=78",
544                         "auth.negTypes=2",
545                         "version=Fred,0.7,1.0,1466",
546                         "lastGoodVersion=Fred,0.7,1.0,1466",
547                         "EndMessage"
548                 );
549                 assertThat(nodeData.get(), notNullValue());
550         }
551
552         @Test
553         public void defaultFcpClientCanGetNodeInformationWithOpennetRef()
554         throws InterruptedException, ExecutionException, IOException {
555                 Future<NodeData> nodeData = fcpClient.getNode().opennetRef().execute();
556                 connectNode();
557                 List<String> lines = fcpServer.collectUntil(is("EndMessage"));
558                 String identifier = extractIdentifier(lines);
559                 assertThat(lines, matchesFcpMessage(
560                         "GetNode",
561                         "Identifier=" + identifier,
562                         "GiveOpennetRef=true",
563                         "WithPrivate=false",
564                         "WithVolatile=false"
565                 ));
566                 fcpServer.writeLine(
567                         "NodeData",
568                         "Identifier=" + identifier,
569                         "opennet=true",
570                         "ark.pubURI=SSK@3YEf.../ark",
571                         "ark.number=78",
572                         "auth.negTypes=2",
573                         "version=Fred,0.7,1.0,1466",
574                         "lastGoodVersion=Fred,0.7,1.0,1466",
575                         "EndMessage"
576                 );
577                 assertThat(nodeData.get().getVersion().toString(), is("Fred,0.7,1.0,1466"));
578         }
579
580         @Test
581         public void defaultFcpClientCanGetNodeInformationWithPrivateData()
582         throws InterruptedException, ExecutionException, IOException {
583                 Future<NodeData> nodeData = fcpClient.getNode().includePrivate().execute();
584                 connectNode();
585                 List<String> lines = fcpServer.collectUntil(is("EndMessage"));
586                 String identifier = extractIdentifier(lines);
587                 assertThat(lines, matchesFcpMessage(
588                         "GetNode",
589                         "Identifier=" + identifier,
590                         "GiveOpennetRef=false",
591                         "WithPrivate=true",
592                         "WithVolatile=false"
593                 ));
594                 fcpServer.writeLine(
595                         "NodeData",
596                         "Identifier=" + identifier,
597                         "opennet=false",
598                         "ark.pubURI=SSK@3YEf.../ark",
599                         "ark.number=78",
600                         "auth.negTypes=2",
601                         "version=Fred,0.7,1.0,1466",
602                         "lastGoodVersion=Fred,0.7,1.0,1466",
603                         "ark.privURI=SSK@XdHMiRl",
604                         "EndMessage"
605                 );
606                 assertThat(nodeData.get().getARK().getPrivateURI(), is("SSK@XdHMiRl"));
607         }
608
609         @Test
610         public void defaultFcpClientCanGetNodeInformationWithVolatileData()
611         throws InterruptedException, ExecutionException, IOException {
612                 Future<NodeData> nodeData = fcpClient.getNode().includeVolatile().execute();
613                 connectNode();
614                 List<String> lines = fcpServer.collectUntil(is("EndMessage"));
615                 String identifier = extractIdentifier(lines);
616                 assertThat(lines, matchesFcpMessage(
617                         "GetNode",
618                         "Identifier=" + identifier,
619                         "GiveOpennetRef=false",
620                         "WithPrivate=false",
621                         "WithVolatile=true"
622                 ));
623                 fcpServer.writeLine(
624                         "NodeData",
625                         "Identifier=" + identifier,
626                         "opennet=false",
627                         "ark.pubURI=SSK@3YEf.../ark",
628                         "ark.number=78",
629                         "auth.negTypes=2",
630                         "version=Fred,0.7,1.0,1466",
631                         "lastGoodVersion=Fred,0.7,1.0,1466",
632                         "volatile.freeJavaMemory=205706528",
633                         "EndMessage"
634                 );
635                 assertThat(nodeData.get().getVolatile("freeJavaMemory"), is("205706528"));
636         }
637
638         @Test
639         public void defaultFcpClientCanGetConfigWithoutDetails()
640         throws InterruptedException, ExecutionException, IOException {
641                 Future<ConfigData> configData = fcpClient.getConfig().execute();
642                 connectNode();
643                 List<String> lines = fcpServer.collectUntil(is("EndMessage"));
644                 String identifier = extractIdentifier(lines);
645                 assertThat(lines, matchesFcpMessage(
646                         "GetConfig",
647                         "Identifier=" + identifier
648                 ));
649                 fcpServer.writeLine(
650                         "ConfigData",
651                         "Identifier=" + identifier,
652                         "EndMessage"
653                 );
654                 assertThat(configData.get(), notNullValue());
655         }
656
657         @Test
658         public void defaultFcpClientCanGetConfigWithCurrent()
659         throws InterruptedException, ExecutionException, IOException {
660                 Future<ConfigData> configData = fcpClient.getConfig().withCurrent().execute();
661                 connectNode();
662                 List<String> lines = fcpServer.collectUntil(is("EndMessage"));
663                 String identifier = extractIdentifier(lines);
664                 assertThat(lines, matchesFcpMessage(
665                         "GetConfig",
666                         "Identifier=" + identifier,
667                         "WithCurrent=true"
668                 ));
669                 fcpServer.writeLine(
670                         "ConfigData",
671                         "Identifier=" + identifier,
672                         "current.foo=bar",
673                         "EndMessage"
674                 );
675                 assertThat(configData.get().getCurrent("foo"), is("bar"));
676         }
677
678         @Test
679         public void defaultFcpClientCanGetConfigWithDefaults()
680         throws InterruptedException, ExecutionException, IOException {
681                 Future<ConfigData> configData = fcpClient.getConfig().withDefaults().execute();
682                 connectNode();
683                 List<String> lines = fcpServer.collectUntil(is("EndMessage"));
684                 String identifier = extractIdentifier(lines);
685                 assertThat(lines, matchesFcpMessage(
686                         "GetConfig",
687                         "Identifier=" + identifier,
688                         "WithDefaults=true"
689                 ));
690                 fcpServer.writeLine(
691                         "ConfigData",
692                         "Identifier=" + identifier,
693                         "default.foo=bar",
694                         "EndMessage"
695                 );
696                 assertThat(configData.get().getDefault("foo"), is("bar"));
697         }
698
699         @Test
700         public void defaultFcpClientCanGetConfigWithSortOrder()
701         throws InterruptedException, ExecutionException, IOException {
702                 Future<ConfigData> configData = fcpClient.getConfig().withSortOrder().execute();
703                 connectNode();
704                 List<String> lines = fcpServer.collectUntil(is("EndMessage"));
705                 String identifier = extractIdentifier(lines);
706                 assertThat(lines, matchesFcpMessage(
707                         "GetConfig",
708                         "Identifier=" + identifier,
709                         "WithSortOrder=true"
710                 ));
711                 fcpServer.writeLine(
712                         "ConfigData",
713                         "Identifier=" + identifier,
714                         "sortOrder.foo=17",
715                         "EndMessage"
716                 );
717                 assertThat(configData.get().getSortOrder("foo"), is(17));
718         }
719
720         @Test
721         public void defaultFcpClientCanGetConfigWithExpertFlag()
722         throws InterruptedException, ExecutionException, IOException {
723                 Future<ConfigData> configData = fcpClient.getConfig().withExpertFlag().execute();
724                 connectNode();
725                 List<String> lines = fcpServer.collectUntil(is("EndMessage"));
726                 String identifier = extractIdentifier(lines);
727                 assertThat(lines, matchesFcpMessage(
728                         "GetConfig",
729                         "Identifier=" + identifier,
730                         "WithExpertFlag=true"
731                 ));
732                 fcpServer.writeLine(
733                         "ConfigData",
734                         "Identifier=" + identifier,
735                         "expertFlag.foo=true",
736                         "EndMessage"
737                 );
738                 assertThat(configData.get().getExpertFlag("foo"), is(true));
739         }
740
741         @Test
742         public void defaultFcpClientCanGetConfigWithForceWriteFlag()
743         throws InterruptedException, ExecutionException, IOException {
744                 Future<ConfigData> configData = fcpClient.getConfig().withForceWriteFlag().execute();
745                 connectNode();
746                 List<String> lines = fcpServer.collectUntil(is("EndMessage"));
747                 String identifier = extractIdentifier(lines);
748                 assertThat(lines, matchesFcpMessage(
749                         "GetConfig",
750                         "Identifier=" + identifier,
751                         "WithForceWriteFlag=true"
752                 ));
753                 fcpServer.writeLine(
754                         "ConfigData",
755                         "Identifier=" + identifier,
756                         "forceWriteFlag.foo=true",
757                         "EndMessage"
758                 );
759                 assertThat(configData.get().getForceWriteFlag("foo"), is(true));
760         }
761
762         @Test
763         public void defaultFcpClientCanGetConfigWithShortDescription()
764         throws InterruptedException, ExecutionException, IOException {
765                 Future<ConfigData> configData = fcpClient.getConfig().withShortDescription().execute();
766                 connectNode();
767                 List<String> lines = fcpServer.collectUntil(is("EndMessage"));
768                 String identifier = extractIdentifier(lines);
769                 assertThat(lines, matchesFcpMessage(
770                         "GetConfig",
771                         "Identifier=" + identifier,
772                         "WithShortDescription=true"
773                 ));
774                 fcpServer.writeLine(
775                         "ConfigData",
776                         "Identifier=" + identifier,
777                         "shortDescription.foo=bar",
778                         "EndMessage"
779                 );
780                 assertThat(configData.get().getShortDescription("foo"), is("bar"));
781         }
782
783         @Test
784         public void defaultFcpClientCanGetConfigWithLongDescription()
785         throws InterruptedException, ExecutionException, IOException {
786                 Future<ConfigData> configData = fcpClient.getConfig().withLongDescription().execute();
787                 connectNode();
788                 List<String> lines = fcpServer.collectUntil(is("EndMessage"));
789                 String identifier = extractIdentifier(lines);
790                 assertThat(lines, matchesFcpMessage(
791                         "GetConfig",
792                         "Identifier=" + identifier,
793                         "WithLongDescription=true"
794                 ));
795                 fcpServer.writeLine(
796                         "ConfigData",
797                         "Identifier=" + identifier,
798                         "longDescription.foo=bar",
799                         "EndMessage"
800                 );
801                 assertThat(configData.get().getLongDescription("foo"), is("bar"));
802         }
803
804         @Test
805         public void defaultFcpClientCanGetConfigWithDataTypes()
806         throws InterruptedException, ExecutionException, IOException {
807                 Future<ConfigData> configData = fcpClient.getConfig().withDataTypes().execute();
808                 connectNode();
809                 List<String> lines = fcpServer.collectUntil(is("EndMessage"));
810                 String identifier = extractIdentifier(lines);
811                 assertThat(lines, matchesFcpMessage(
812                         "GetConfig",
813                         "Identifier=" + identifier,
814                         "WithDataTypes=true"
815                 ));
816                 fcpServer.writeLine(
817                         "ConfigData",
818                         "Identifier=" + identifier,
819                         "dataType.foo=number",
820                         "EndMessage"
821                 );
822                 assertThat(configData.get().getDataType("foo"), is("number"));
823         }
824
825         @Test
826         public void defaultFcpClientCanModifyConfigData() throws InterruptedException, ExecutionException, IOException {
827                 Future<ConfigData> newConfigData = fcpClient.modifyConfig().set("foo.bar").to("baz").execute();
828                 connectNode();
829                 List<String> lines = fcpServer.collectUntil(is("EndMessage"));
830                 String identifier = extractIdentifier(lines);
831                 assertThat(lines, matchesFcpMessage(
832                         "ModifyConfig",
833                         "Identifier=" + identifier,
834                         "foo.bar=baz"
835                 ));
836                 fcpServer.writeLine(
837                         "ConfigData",
838                         "Identifier=" + identifier,
839                         "current.foo.bar=baz",
840                         "EndMessage"
841                 );
842                 assertThat(newConfigData.get().getCurrent("foo.bar"), is("baz"));
843         }
844
845         private List<String> lines;
846         private String identifier;
847
848         private void connectAndAssert(Supplier<Matcher<List<String>>> requestMatcher)
849         throws InterruptedException, ExecutionException, IOException {
850                 connectNode();
851                 readMessage(requestMatcher);
852         }
853
854         private void readMessage(Supplier<Matcher<List<String>>> requestMatcher) throws IOException {
855                 lines = fcpServer.collectUntil(is("EndMessage"));
856                 identifier = extractIdentifier(lines);
857                 assertThat(lines, requestMatcher.get());
858         }
859
860         public class ConnectionsAndKeyPairs {
861
862                 public class Connections {
863
864                         @Test(expected = ExecutionException.class)
865                         public void throwsExceptionOnFailure() throws IOException, ExecutionException, InterruptedException {
866                                 Future<FcpKeyPair> keyPairFuture = fcpClient.generateKeypair().execute();
867                                 connectAndAssert(() -> matchesFcpMessage("GenerateSSK"));
868                                 fcpServer.writeLine(
869                                         "CloseConnectionDuplicateClientName",
870                                         "EndMessage"
871                                 );
872                                 keyPairFuture.get();
873                         }
874
875                         @Test(expected = ExecutionException.class)
876                         public void throwsExceptionIfConnectionIsClosed() throws IOException, ExecutionException, InterruptedException {
877                                 Future<FcpKeyPair> keyPairFuture = fcpClient.generateKeypair().execute();
878                                 connectAndAssert(() -> matchesFcpMessage("GenerateSSK"));
879                                 fcpServer.close();
880                                 keyPairFuture.get();
881                         }
882
883                         @Test
884                         public void connectionIsReused() throws InterruptedException, ExecutionException, IOException {
885                                 Future<FcpKeyPair> keyPair = fcpClient.generateKeypair().execute();
886                                 connectAndAssert(() -> matchesFcpMessage("GenerateSSK"));
887                                 replyWithKeyPair();
888                                 keyPair.get();
889                                 keyPair = fcpClient.generateKeypair().execute();
890                                 readMessage(() -> matchesFcpMessage("GenerateSSK"));
891                                 identifier = extractIdentifier(lines);
892                                 replyWithKeyPair();
893                                 keyPair.get();
894                         }
895
896                         @Test
897                         public void defaultFcpClientCanReconnectAfterConnectionHasBeenClosed()
898                         throws InterruptedException, ExecutionException, IOException {
899                                 Future<FcpKeyPair> keyPair = fcpClient.generateKeypair().execute();
900                                 connectAndAssert(() -> matchesFcpMessage("GenerateSSK"));
901                                 fcpServer.close();
902                                 try {
903                                         keyPair.get();
904                                         Assert.fail();
905                                 } catch (ExecutionException e) {
906                                 }
907                                 keyPair = fcpClient.generateKeypair().execute();
908                                 connectAndAssert(() -> matchesFcpMessage("GenerateSSK"));
909                                 replyWithKeyPair();
910                                 keyPair.get();
911                         }
912
913                 }
914
915                 public class GenerateKeyPair {
916
917                         @Test
918                         public void defaultFcpClientCanGenerateKeypair()
919                         throws ExecutionException, InterruptedException, IOException {
920                                 Future<FcpKeyPair> keyPairFuture = fcpClient.generateKeypair().execute();
921                                 connectAndAssert(() -> matchesFcpMessage("GenerateSSK"));
922                                 replyWithKeyPair();
923                                 FcpKeyPair keyPair = keyPairFuture.get();
924                                 assertThat(keyPair.getPublicKey(), is(REQUEST_URI));
925                                 assertThat(keyPair.getPrivateKey(), is(INSERT_URI));
926                         }
927
928                 }
929
930                 private void replyWithKeyPair() throws IOException {
931                         fcpServer.writeLine("SSKKeypair",
932                                 "InsertURI=" + INSERT_URI + "",
933                                 "RequestURI=" + REQUEST_URI + "",
934                                 "Identifier=" + identifier,
935                                 "EndMessage");
936                 }
937
938         }
939
940         public class Peers {
941
942                 public class PeerCommands {
943
944                         public class ListPeer {
945
946                                 @Test
947                                 public void byIdentity() throws InterruptedException, ExecutionException, IOException {
948                                         Future<Optional<Peer>> peer = fcpClient.listPeer().byIdentity("id1").execute();
949                                         connectAndAssert(() -> matchesListPeer("id1"));
950                                         replyWithPeer("id1");
951                                         assertThat(peer.get().get().getIdentity(), is("id1"));
952                                 }
953
954                                 @Test
955                                 public void byHostAndPort() throws InterruptedException, ExecutionException, IOException {
956                                         Future<Optional<Peer>> peer = fcpClient.listPeer().byHostAndPort("host.free.net", 12345).execute();
957                                         connectAndAssert(() -> matchesListPeer("host.free.net:12345"));
958                                         replyWithPeer("id1");
959                                         assertThat(peer.get().get().getIdentity(), is("id1"));
960                                 }
961
962                                 @Test
963                                 public void byName() throws InterruptedException, ExecutionException, IOException {
964                                         Future<Optional<Peer>> peer = fcpClient.listPeer().byName("FriendNode").execute();
965                                         connectAndAssert(() -> matchesListPeer("FriendNode"));
966                                         replyWithPeer("id1");
967                                         assertThat(peer.get().get().getIdentity(), is("id1"));
968                                 }
969
970                                 @Test
971                                 public void unknownNodeIdentifier() throws InterruptedException, ExecutionException, IOException {
972                                         Future<Optional<Peer>> peer = fcpClient.listPeer().byIdentity("id2").execute();
973                                         connectAndAssert(() -> matchesListPeer("id2"));
974                                         replyWithUnknownNodeIdentifier();
975                                         assertThat(peer.get().isPresent(), is(false));
976                                 }
977
978                                 private Matcher<List<String>> matchesListPeer(String nodeId) {
979                                         return matchesFcpMessage(
980                                                 "ListPeer",
981                                                 "Identifier=" + identifier,
982                                                 "NodeIdentifier=" + nodeId
983                                         );
984                                 }
985
986                         }
987
988                         public class ListPeers {
989
990                                 @Test
991                                 public void withoutMetadataOrVolatile() throws IOException, ExecutionException, InterruptedException {
992                                         Future<Collection<Peer>> peers = fcpClient.listPeers().execute();
993                                         connectAndAssert(() -> matchesListPeers(false, false));
994                                         replyWithPeer("id1");
995                                         replyWithPeer("id2");
996                                         sendEndOfPeerList();
997                                         assertThat(peers.get(), hasSize(2));
998                                         assertThat(peers.get().stream().map(Peer::getIdentity).collect(Collectors.toList()),
999                                                 containsInAnyOrder("id1", "id2"));
1000                                 }
1001
1002                                 @Test
1003                                 public void withMetadata() throws IOException, ExecutionException, InterruptedException {
1004                                         Future<Collection<Peer>> peers = fcpClient.listPeers().includeMetadata().execute();
1005                                         connectAndAssert(() -> matchesListPeers(false, true));
1006                                         replyWithPeer("id1", "metadata.foo=bar1");
1007                                         replyWithPeer("id2", "metadata.foo=bar2");
1008                                         sendEndOfPeerList();
1009                                         assertThat(peers.get(), hasSize(2));
1010                                         assertThat(peers.get().stream().map(peer -> peer.getMetadata("foo")).collect(Collectors.toList()),
1011                                                 containsInAnyOrder("bar1", "bar2"));
1012                                 }
1013
1014                                 @Test
1015                                 public void withVolatile() throws IOException, ExecutionException, InterruptedException {
1016                                         Future<Collection<Peer>> peers = fcpClient.listPeers().includeVolatile().execute();
1017                                         connectAndAssert(() -> matchesListPeers(true, false));
1018                                         replyWithPeer("id1", "volatile.foo=bar1");
1019                                         replyWithPeer("id2", "volatile.foo=bar2");
1020                                         sendEndOfPeerList();
1021                                         assertThat(peers.get(), hasSize(2));
1022                                         assertThat(peers.get().stream().map(peer -> peer.getVolatile("foo")).collect(Collectors.toList()),
1023                                                 containsInAnyOrder("bar1", "bar2"));
1024                                 }
1025
1026                                 private Matcher<List<String>> matchesListPeers(boolean withVolatile, boolean withMetadata) {
1027                                         return matchesFcpMessage(
1028                                                 "ListPeers",
1029                                                 "WithVolatile=" + withVolatile,
1030                                                 "WithMetadata=" + withMetadata
1031                                         );
1032                                 }
1033
1034                                 private void sendEndOfPeerList() throws IOException {
1035                                         fcpServer.writeLine(
1036                                                 "EndListPeers",
1037                                                 "Identifier=" + identifier,
1038                                                 "EndMessage"
1039                                         );
1040                                 }
1041
1042                         }
1043
1044                         public class AddPeer {
1045
1046                                 @Test
1047                                 public void fromFile() throws InterruptedException, ExecutionException, IOException {
1048                                         Future<Optional<Peer>> peer = fcpClient.addPeer().fromFile(new File("/tmp/ref.txt")).execute();
1049                                         connectAndAssert(() -> allOf(matchesAddPeer(), hasItem("File=/tmp/ref.txt")));
1050                                         replyWithPeer("id1");
1051                                         assertThat(peer.get().get().getIdentity(), is("id1"));
1052                                 }
1053
1054                                 @Test
1055                                 public void fromUrl() throws InterruptedException, ExecutionException, IOException {
1056                                         Future<Optional<Peer>> peer = fcpClient.addPeer().fromURL(new URL("http://node.ref/")).execute();
1057                                         connectAndAssert(() -> allOf(matchesAddPeer(), hasItem("URL=http://node.ref/")));
1058                                         replyWithPeer("id1");
1059                                         assertThat(peer.get().get().getIdentity(), is("id1"));
1060                                 }
1061
1062                                 @Test
1063                                 public void fromNodeRef() throws InterruptedException, ExecutionException, IOException {
1064                                         NodeRef nodeRef = createNodeRef();
1065                                         Future<Optional<Peer>> peer = fcpClient.addPeer().fromNodeRef(nodeRef).execute();
1066                                         connectAndAssert(() -> allOf(matchesAddPeer(), Matchers.<String>hasItems(
1067                                                 "myName=name",
1068                                                 "ark.pubURI=public",
1069                                                 "ark.number=1",
1070                                                 "dsaGroup.g=base",
1071                                                 "dsaGroup.p=prime",
1072                                                 "dsaGroup.q=subprime",
1073                                                 "dsaPubKey.y=dsa-public",
1074                                                 "physical.udp=1.2.3.4:5678",
1075                                                 "auth.negTypes=3;5",
1076                                                 "sig=sig"
1077                                         )));
1078                                         replyWithPeer("id1");
1079                                         assertThat(peer.get().get().getIdentity(), is("id1"));
1080                                 }
1081
1082                                 private NodeRef createNodeRef() {
1083                                         NodeRef nodeRef = new NodeRef();
1084                                         nodeRef.setIdentity("id1");
1085                                         nodeRef.setName("name");
1086                                         nodeRef.setARK(new ARK("public", "1"));
1087                                         nodeRef.setDSAGroup(new DSAGroup("base", "prime", "subprime"));
1088                                         nodeRef.setNegotiationTypes(new int[] { 3, 5 });
1089                                         nodeRef.setPhysicalUDP("1.2.3.4:5678");
1090                                         nodeRef.setDSAPublicKey("dsa-public");
1091                                         nodeRef.setSignature("sig");
1092                                         return nodeRef;
1093                                 }
1094
1095                                 private Matcher<List<String>> matchesAddPeer() {
1096                                         return matchesFcpMessage(
1097                                                 "AddPeer",
1098                                                 "Identifier=" + identifier
1099                                         );
1100                                 }
1101
1102                         }
1103
1104                         public class ModifyPeer {
1105
1106                                 @Test
1107                                 public void defaultFcpClientCanEnablePeerByName()
1108                                 throws InterruptedException, ExecutionException, IOException {
1109                                         Future<Optional<Peer>> peer = fcpClient.modifyPeer().enable().byName("id1").execute();
1110                                         connectAndAssert(() -> matchesModifyPeer("id1", "IsDisabled", false));
1111                                         replyWithPeer("id1");
1112                                         assertThat(peer.get().get().getIdentity(), is("id1"));
1113                                 }
1114
1115                                 @Test
1116                                 public void defaultFcpClientCanDisablePeerByName()
1117                                 throws InterruptedException, ExecutionException, IOException {
1118                                         Future<Optional<Peer>> peer = fcpClient.modifyPeer().disable().byName("id1").execute();
1119                                         connectAndAssert(() -> matchesModifyPeer("id1", "IsDisabled", true));
1120                                         replyWithPeer("id1");
1121                                         assertThat(peer.get().get().getIdentity(), is("id1"));
1122                                 }
1123
1124                                 @Test
1125                                 public void defaultFcpClientCanEnablePeerByIdentity()
1126                                 throws InterruptedException, ExecutionException, IOException {
1127                                         Future<Optional<Peer>> peer = fcpClient.modifyPeer().enable().byIdentity("id1").execute();
1128                                         connectAndAssert(() -> matchesModifyPeer("id1", "IsDisabled", false));
1129                                         replyWithPeer("id1");
1130                                         assertThat(peer.get().get().getIdentity(), is("id1"));
1131                                 }
1132
1133                                 @Test
1134                                 public void defaultFcpClientCanEnablePeerByHostAndPort()
1135                                 throws InterruptedException, ExecutionException, IOException {
1136                                         Future<Optional<Peer>> peer =
1137                                                 fcpClient.modifyPeer().enable().byHostAndPort("1.2.3.4", 5678).execute();
1138                                         connectAndAssert(() -> matchesModifyPeer("1.2.3.4:5678", "IsDisabled", false));
1139                                         replyWithPeer("id1");
1140                                         assertThat(peer.get().get().getIdentity(), is("id1"));
1141                                 }
1142
1143                                 @Test
1144                                 public void allowLocalAddressesOfPeer() throws InterruptedException, ExecutionException, IOException {
1145                                         Future<Optional<Peer>> peer =
1146                                                 fcpClient.modifyPeer().allowLocalAddresses().byIdentity("id1").execute();
1147                                         connectAndAssert(() -> allOf(
1148                                                 matchesModifyPeer("id1", "AllowLocalAddresses", true),
1149                                                 not(contains(startsWith("IsDisabled=")))
1150                                         ));
1151                                         replyWithPeer("id1");
1152                                         assertThat(peer.get().get().getIdentity(), is("id1"));
1153                                 }
1154
1155                                 @Test
1156                                 public void disallowLocalAddressesOfPeer()
1157                                 throws InterruptedException, ExecutionException, IOException {
1158                                         Future<Optional<Peer>> peer =
1159                                                 fcpClient.modifyPeer().disallowLocalAddresses().byIdentity("id1").execute();
1160                                         connectAndAssert(() -> allOf(
1161                                                 matchesModifyPeer("id1", "AllowLocalAddresses", false),
1162                                                 not(contains(startsWith("IsDisabled=")))
1163                                         ));
1164                                         replyWithPeer("id1");
1165                                         assertThat(peer.get().get().getIdentity(), is("id1"));
1166                                 }
1167
1168                                 @Test
1169                                 public void setBurstOnlyForPeer() throws InterruptedException, ExecutionException, IOException {
1170                                         Future<Optional<Peer>> peer = fcpClient.modifyPeer().setBurstOnly().byIdentity("id1").execute();
1171                                         connectAndAssert(() -> allOf(
1172                                                 matchesModifyPeer("id1", "IsBurstOnly", true),
1173                                                 not(contains(startsWith("AllowLocalAddresses="))),
1174                                                 not(contains(startsWith("IsDisabled=")))
1175                                         ));
1176                                         replyWithPeer("id1");
1177                                         assertThat(peer.get().get().getIdentity(), is("id1"));
1178                                 }
1179
1180                                 @Test
1181                                 public void clearBurstOnlyForPeer() throws InterruptedException, ExecutionException, IOException {
1182                                         Future<Optional<Peer>> peer = fcpClient.modifyPeer().clearBurstOnly().byIdentity("id1").execute();
1183                                         connectAndAssert(() -> allOf(
1184                                                 matchesModifyPeer("id1", "IsBurstOnly", false),
1185                                                 not(contains(startsWith("AllowLocalAddresses="))),
1186                                                 not(contains(startsWith("IsDisabled=")))
1187                                         ));
1188                                         replyWithPeer("id1");
1189                                         assertThat(peer.get().get().getIdentity(), is("id1"));
1190                                 }
1191
1192                                 @Test
1193                                 public void defaultFcpClientCanSetListenOnlyForPeer()
1194                                 throws InterruptedException, ExecutionException, IOException {
1195                                         Future<Optional<Peer>> peer = fcpClient.modifyPeer().setListenOnly().byIdentity("id1").execute();
1196                                         connectAndAssert(() -> allOf(
1197                                                 matchesModifyPeer("id1", "IsListenOnly", true),
1198                                                 not(contains(startsWith("AllowLocalAddresses="))),
1199                                                 not(contains(startsWith("IsDisabled="))),
1200                                                 not(contains(startsWith("IsBurstOnly=")))
1201                                         ));
1202                                         replyWithPeer("id1");
1203                                         assertThat(peer.get().get().getIdentity(), is("id1"));
1204                                 }
1205
1206                                 @Test
1207                                 public void clearListenOnlyForPeer() throws InterruptedException, ExecutionException, IOException {
1208                                         Future<Optional<Peer>> peer = fcpClient.modifyPeer().clearListenOnly().byIdentity("id1").execute();
1209                                         connectAndAssert(() -> allOf(
1210                                                 matchesModifyPeer("id1", "IsListenOnly", false),
1211                                                 not(contains(startsWith("AllowLocalAddresses="))),
1212                                                 not(contains(startsWith("IsDisabled="))),
1213                                                 not(contains(startsWith("IsBurstOnly=")))
1214                                         ));
1215                                         replyWithPeer("id1");
1216                                         assertThat(peer.get().get().getIdentity(), is("id1"));
1217                                 }
1218
1219                                 @Test
1220                                 public void ignoreSourceForPeer() throws InterruptedException, ExecutionException, IOException {
1221                                         Future<Optional<Peer>> peer = fcpClient.modifyPeer().ignoreSource().byIdentity("id1").execute();
1222                                         connectAndAssert(() -> allOf(
1223                                                 matchesModifyPeer("id1", "IgnoreSourcePort", true),
1224                                                 not(contains(startsWith("AllowLocalAddresses="))),
1225                                                 not(contains(startsWith("IsDisabled="))),
1226                                                 not(contains(startsWith("IsBurstOnly="))),
1227                                                 not(contains(startsWith("IsListenOnly=")))
1228                                         ));
1229                                         replyWithPeer("id1");
1230                                         assertThat(peer.get().get().getIdentity(), is("id1"));
1231                                 }
1232
1233                                 @Test
1234                                 public void useSourceForPeer() throws InterruptedException, ExecutionException, IOException {
1235                                         Future<Optional<Peer>> peer = fcpClient.modifyPeer().useSource().byIdentity("id1").execute();
1236                                         connectAndAssert(() -> allOf(
1237                                                 matchesModifyPeer("id1", "IgnoreSourcePort", false),
1238                                                 not(contains(startsWith("AllowLocalAddresses="))),
1239                                                 not(contains(startsWith("IsDisabled="))),
1240                                                 not(contains(startsWith("IsBurstOnly="))),
1241                                                 not(contains(startsWith("IsListenOnly=")))
1242                                         ));
1243                                         replyWithPeer("id1");
1244                                         assertThat(peer.get().get().getIdentity(), is("id1"));
1245                                 }
1246
1247                                 @Test
1248                                 public void unknownNode() throws InterruptedException, ExecutionException, IOException {
1249                                         Future<Optional<Peer>> peer = fcpClient.modifyPeer().enable().byIdentity("id1").execute();
1250                                         connectAndAssert(() -> matchesModifyPeer("id1", "IsDisabled", false));
1251                                         replyWithUnknownNodeIdentifier();
1252                                         assertThat(peer.get().isPresent(), is(false));
1253                                 }
1254
1255                                 private Matcher<List<String>> matchesModifyPeer(String nodeIdentifier, String setting, boolean value) {
1256                                         return matchesFcpMessage(
1257                                                 "ModifyPeer",
1258                                                 "Identifier=" + identifier,
1259                                                 "NodeIdentifier=" + nodeIdentifier,
1260                                                 setting + "=" + value
1261                                         );
1262                                 }
1263
1264                         }
1265
1266                         public class RemovePeer {
1267
1268                                 @Test
1269                                 public void byName() throws InterruptedException, ExecutionException, IOException {
1270                                         Future<Boolean> peer = fcpClient.removePeer().byName("Friend1").execute();
1271                                         connectAndAssert(() -> matchesRemovePeer("Friend1"));
1272                                         replyWithPeerRemoved("Friend1");
1273                                         assertThat(peer.get(), is(true));
1274                                 }
1275
1276                                 @Test
1277                                 public void invalidName() throws InterruptedException, ExecutionException, IOException {
1278                                         Future<Boolean> peer = fcpClient.removePeer().byName("NotFriend1").execute();
1279                                         connectAndAssert(() -> matchesRemovePeer("NotFriend1"));
1280                                         replyWithUnknownNodeIdentifier();
1281                                         assertThat(peer.get(), is(false));
1282                                 }
1283
1284                                 @Test
1285                                 public void byIdentity() throws InterruptedException, ExecutionException, IOException {
1286                                         Future<Boolean> peer = fcpClient.removePeer().byIdentity("id1").execute();
1287                                         connectAndAssert(() -> matchesRemovePeer("id1"));
1288                                         replyWithPeerRemoved("id1");
1289                                         assertThat(peer.get(), is(true));
1290                                 }
1291
1292                                 @Test
1293                                 public void byHostAndPort() throws InterruptedException, ExecutionException, IOException {
1294                                         Future<Boolean> peer = fcpClient.removePeer().byHostAndPort("1.2.3.4", 5678).execute();
1295                                         connectAndAssert(() -> matchesRemovePeer("1.2.3.4:5678"));
1296                                         replyWithPeerRemoved("Friend1");
1297                                         assertThat(peer.get(), is(true));
1298                                 }
1299
1300                                 private Matcher<List<String>> matchesRemovePeer(String nodeIdentifier) {
1301                                         return matchesFcpMessage(
1302                                                 "RemovePeer",
1303                                                 "Identifier=" + identifier,
1304                                                 "NodeIdentifier=" + nodeIdentifier
1305                                         );
1306                                 }
1307
1308                                 private void replyWithPeerRemoved(String nodeIdentifier) throws IOException {
1309                                         fcpServer.writeLine(
1310                                                 "PeerRemoved",
1311                                                 "Identifier=" + identifier,
1312                                                 "NodeIdentifier=" + nodeIdentifier,
1313                                                 "EndMessage"
1314                                         );
1315                                 }
1316
1317                         }
1318
1319                         private void replyWithPeer(String peerId, String... additionalLines) throws IOException {
1320                                 fcpServer.writeLine(
1321                                         "Peer",
1322                                         "Identifier=" + identifier,
1323                                         "identity=" + peerId,
1324                                         "opennet=false",
1325                                         "ark.pubURI=SSK@3YEf.../ark",
1326                                         "ark.number=78",
1327                                         "auth.negTypes=2",
1328                                         "version=Fred,0.7,1.0,1466",
1329                                         "lastGoodVersion=Fred,0.7,1.0,1466"
1330                                 );
1331                                 fcpServer.writeLine(additionalLines);
1332                                 fcpServer.writeLine("EndMessage");
1333                         }
1334
1335                 }
1336
1337                 public class PeerNoteCommands {
1338
1339                         public class ListPeerNotes {
1340
1341                                 @Test
1342                                 public void onUnknownNodeIdentifier() throws InterruptedException, ExecutionException, IOException {
1343                                         Future<Optional<PeerNote>> peerNote = fcpClient.listPeerNotes().byName("Friend1").execute();
1344                                         connectAndAssert(() -> matchesListPeerNotes("Friend1"));
1345                                         replyWithUnknownNodeIdentifier();
1346                                         assertThat(peerNote.get().isPresent(), is(false));
1347                                 }
1348
1349                                 @Test
1350                                 public void byNodeName() throws InterruptedException, ExecutionException, IOException {
1351                                         Future<Optional<PeerNote>> peerNote = fcpClient.listPeerNotes().byName("Friend1").execute();
1352                                         connectAndAssert(() -> matchesListPeerNotes("Friend1"));
1353                                         replyWithPeerNote();
1354                                         replyWithEndListPeerNotes();
1355                                         assertThat(peerNote.get().get().getNoteText(), is("RXhhbXBsZSBUZXh0Lg=="));
1356                                         assertThat(peerNote.get().get().getPeerNoteType(), is(1));
1357                                 }
1358
1359                                 @Test
1360                                 public void byNodeIdentifier() throws InterruptedException, ExecutionException, IOException {
1361                                         Future<Optional<PeerNote>> peerNote = fcpClient.listPeerNotes().byIdentity("id1").execute();
1362                                         connectAndAssert(() -> matchesListPeerNotes("id1"));
1363                                         replyWithPeerNote();
1364                                         replyWithEndListPeerNotes();
1365                                         assertThat(peerNote.get().get().getNoteText(), is("RXhhbXBsZSBUZXh0Lg=="));
1366                                         assertThat(peerNote.get().get().getPeerNoteType(), is(1));
1367                                 }
1368
1369                                 @Test
1370                                 public void byHostAndPort() throws InterruptedException, ExecutionException, IOException {
1371                                         Future<Optional<PeerNote>> peerNote =
1372                                                 fcpClient.listPeerNotes().byHostAndPort("1.2.3.4", 5678).execute();
1373                                         connectAndAssert(() -> matchesListPeerNotes("1.2.3.4:5678"));
1374                                         replyWithPeerNote();
1375                                         replyWithEndListPeerNotes();
1376                                         assertThat(peerNote.get().get().getNoteText(), is("RXhhbXBsZSBUZXh0Lg=="));
1377                                         assertThat(peerNote.get().get().getPeerNoteType(), is(1));
1378                                 }
1379
1380                                 private Matcher<List<String>> matchesListPeerNotes(String nodeIdentifier) {
1381                                         return matchesFcpMessage(
1382                                                 "ListPeerNotes",
1383                                                 "NodeIdentifier=" + nodeIdentifier
1384                                         );
1385                                 }
1386
1387                                 private void replyWithEndListPeerNotes() throws IOException {
1388                                         fcpServer.writeLine(
1389                                                 "EndListPeerNotes",
1390                                                 "Identifier=" + identifier,
1391                                                 "EndMessage"
1392                                         );
1393                                 }
1394
1395                                 private void replyWithPeerNote() throws IOException {
1396                                         fcpServer.writeLine(
1397                                                 "PeerNote",
1398                                                 "Identifier=" + identifier,
1399                                                 "NodeIdentifier=Friend1",
1400                                                 "NoteText=RXhhbXBsZSBUZXh0Lg==",
1401                                                 "PeerNoteType=1",
1402                                                 "EndMessage"
1403                                         );
1404                                 }
1405
1406                         }
1407
1408                         public class ModifyPeerNotes {
1409
1410                                 @Test
1411                                 public void byName() throws InterruptedException, ExecutionException, IOException {
1412                                         Future<Boolean> noteUpdated =
1413                                                 fcpClient.modifyPeerNote().darknetComment("foo").byName("Friend1").execute();
1414                                         connectAndAssert(() -> matchesModifyPeerNote("Friend1"));
1415                                         replyWithPeerNote();
1416                                         assertThat(noteUpdated.get(), is(true));
1417                                 }
1418
1419                                 @Test
1420                                 public void onUnknownNodeIdentifier() throws InterruptedException, ExecutionException, IOException {
1421                                         Future<Boolean> noteUpdated =
1422                                                 fcpClient.modifyPeerNote().darknetComment("foo").byName("Friend1").execute();
1423                                         connectAndAssert(() -> matchesModifyPeerNote("Friend1"));
1424                                         replyWithUnknownNodeIdentifier();
1425                                         assertThat(noteUpdated.get(), is(false));
1426                                 }
1427
1428                                 @Test
1429                                 public void defaultFcpClientFailsToModifyPeerNoteWithoutPeerNote()
1430                                 throws InterruptedException, ExecutionException, IOException {
1431                                         Future<Boolean> noteUpdated = fcpClient.modifyPeerNote().byName("Friend1").execute();
1432                                         assertThat(noteUpdated.get(), is(false));
1433                                 }
1434
1435                                 @Test
1436                                 public void byIdentifier() throws InterruptedException, ExecutionException, IOException {
1437                                         Future<Boolean> noteUpdated =
1438                                                 fcpClient.modifyPeerNote().darknetComment("foo").byIdentifier("id1").execute();
1439                                         connectAndAssert(() -> matchesModifyPeerNote("id1"));
1440                                         replyWithPeerNote();
1441                                         assertThat(noteUpdated.get(), is(true));
1442                                 }
1443
1444                                 @Test
1445                                 public void byHostAndPort() throws InterruptedException, ExecutionException, IOException {
1446                                         Future<Boolean> noteUpdated =
1447                                                 fcpClient.modifyPeerNote().darknetComment("foo").byHostAndPort("1.2.3.4", 5678).execute();
1448                                         connectAndAssert(() -> matchesModifyPeerNote("1.2.3.4:5678"));
1449                                         replyWithPeerNote();
1450                                         assertThat(noteUpdated.get(), is(true));
1451                                 }
1452
1453                                 private Matcher<List<String>> matchesModifyPeerNote(String nodeIdentifier) {
1454                                         return matchesFcpMessage(
1455                                                 "ModifyPeerNote",
1456                                                 "Identifier=" + identifier,
1457                                                 "NodeIdentifier=" + nodeIdentifier,
1458                                                 "PeerNoteType=1",
1459                                                 "NoteText=Zm9v"
1460                                         );
1461                                 }
1462
1463                                 private void replyWithPeerNote() throws IOException {
1464                                         fcpServer.writeLine(
1465                                                 "PeerNote",
1466                                                 "Identifier=" + identifier,
1467                                                 "NodeIdentifier=Friend1",
1468                                                 "NoteText=Zm9v",
1469                                                 "PeerNoteType=1",
1470                                                 "EndMessage"
1471                                         );
1472                                 }
1473
1474                         }
1475
1476                 }
1477
1478                 private void replyWithUnknownNodeIdentifier() throws IOException {
1479                         fcpServer.writeLine(
1480                                 "UnknownNodeIdentifier",
1481                                 "Identifier=" + identifier,
1482                                 "NodeIdentifier=id2",
1483                                 "EndMessage"
1484                         );
1485                 }
1486
1487         }
1488
1489         public class PluginCommands {
1490
1491                 private static final String CLASS_NAME = "foo.plugin.Plugin";
1492
1493                 private void replyWithPluginInfo() throws IOException {
1494                         fcpServer.writeLine(
1495                                 "PluginInfo",
1496                                 "Identifier=" + identifier,
1497                                 "PluginName=superPlugin",
1498                                 "IsTalkable=true",
1499                                 "LongVersion=1.2.3",
1500                                 "Version=42",
1501                                 "OriginUri=superPlugin",
1502                                 "Started=true",
1503                                 "EndMessage"
1504                         );
1505                 }
1506
1507                 private void verifyPluginInfo(Future<Optional<PluginInfo>> pluginInfo)
1508                 throws InterruptedException, ExecutionException {
1509                         assertThat(pluginInfo.get().get().getPluginName(), is("superPlugin"));
1510                         assertThat(pluginInfo.get().get().getOriginalURI(), is("superPlugin"));
1511                         assertThat(pluginInfo.get().get().isTalkable(), is(true));
1512                         assertThat(pluginInfo.get().get().getVersion(), is("42"));
1513                         assertThat(pluginInfo.get().get().getLongVersion(), is("1.2.3"));
1514                         assertThat(pluginInfo.get().get().isStarted(), is(true));
1515                 }
1516
1517                 public class LoadPlugin {
1518
1519                         public class OfficialPlugins {
1520
1521                                 @Test
1522                                 public void fromFreenet() throws ExecutionException, InterruptedException, IOException {
1523                                         Future<Optional<PluginInfo>> pluginInfo =
1524                                                 fcpClient.loadPlugin().officialFromFreenet("superPlugin").execute();
1525                                         connectAndAssert(() -> createMatcherForOfficialSource("freenet"));
1526                                         assertThat(lines, not(contains(startsWith("Store="))));
1527                                         replyWithPluginInfo();
1528                                         verifyPluginInfo(pluginInfo);
1529                                 }
1530
1531                                 @Test
1532                                 public void persistentFromFreenet() throws ExecutionException, InterruptedException, IOException {
1533                                         Future<Optional<PluginInfo>> pluginInfo =
1534                                                 fcpClient.loadPlugin().addToConfig().officialFromFreenet("superPlugin").execute();
1535                                         connectAndAssert(() -> createMatcherForOfficialSource("freenet"));
1536                                         assertThat(lines, hasItem("Store=true"));
1537                                         replyWithPluginInfo();
1538                                         verifyPluginInfo(pluginInfo);
1539                                 }
1540
1541                                 @Test
1542                                 public void fromHttps() throws ExecutionException, InterruptedException, IOException {
1543                                         Future<Optional<PluginInfo>> pluginInfo =
1544                                                 fcpClient.loadPlugin().officialFromHttps("superPlugin").execute();
1545                                         connectAndAssert(() -> createMatcherForOfficialSource("https"));
1546                                         replyWithPluginInfo();
1547                                         verifyPluginInfo(pluginInfo);
1548                                 }
1549
1550                                 private Matcher<List<String>> createMatcherForOfficialSource(String officialSource) {
1551                                         return matchesFcpMessage(
1552                                                 "LoadPlugin",
1553                                                 "Identifier=" + identifier,
1554                                                 "PluginURL=superPlugin",
1555                                                 "URLType=official",
1556                                                 "OfficialSource=" + officialSource
1557                                         );
1558                                 }
1559
1560                         }
1561
1562                         public class FromOtherSources {
1563
1564                                 private static final String FILE_PATH = "/path/to/plugin.jar";
1565                                 private static final String URL = "http://server.com/plugin.jar";
1566                                 private static final String KEY = "KSK@plugin.jar";
1567
1568                                 @Test
1569                                 public void fromFile() throws ExecutionException, InterruptedException, IOException {
1570                                         Future<Optional<PluginInfo>> pluginInfo = fcpClient.loadPlugin().fromFile(FILE_PATH).execute();
1571                                         connectAndAssert(() -> createMatcher("file", FILE_PATH));
1572                                         replyWithPluginInfo();
1573                                         verifyPluginInfo(pluginInfo);
1574                                 }
1575
1576                                 @Test
1577                                 public void fromUrl() throws ExecutionException, InterruptedException, IOException {
1578                                         Future<Optional<PluginInfo>> pluginInfo = fcpClient.loadPlugin().fromUrl(URL).execute();
1579                                         connectAndAssert(() -> createMatcher("url", URL));
1580                                         replyWithPluginInfo();
1581                                         verifyPluginInfo(pluginInfo);
1582                                 }
1583
1584                                 @Test
1585                                 public void fromFreenet() throws ExecutionException, InterruptedException, IOException {
1586                                         Future<Optional<PluginInfo>> pluginInfo = fcpClient.loadPlugin().fromFreenet(KEY).execute();
1587                                         connectAndAssert(() -> createMatcher("freenet", KEY));
1588                                         replyWithPluginInfo();
1589                                         verifyPluginInfo(pluginInfo);
1590                                 }
1591
1592                                 private Matcher<List<String>> createMatcher(String urlType, String url) {
1593                                         return matchesFcpMessage(
1594                                                 "LoadPlugin",
1595                                                 "Identifier=" + identifier,
1596                                                 "PluginURL=" + url,
1597                                                 "URLType=" + urlType
1598                                         );
1599                                 }
1600
1601                         }
1602
1603                         public class Failed {
1604
1605                                 @Test
1606                                 public void failedLoad() throws ExecutionException, InterruptedException, IOException {
1607                                         Future<Optional<PluginInfo>> pluginInfo =
1608                                                 fcpClient.loadPlugin().officialFromFreenet("superPlugin").execute();
1609                                         connectAndAssert(() -> matchesFcpMessage("LoadPlugin"));
1610                                         replyWithProtocolError();
1611                                         assertThat(pluginInfo.get().isPresent(), is(false));
1612                                 }
1613
1614                         }
1615
1616                 }
1617
1618                 private void replyWithProtocolError() throws IOException {
1619                         fcpServer.writeLine(
1620                                 "ProtocolError",
1621                                 "Identifier=" + identifier,
1622                                 "EndMessage"
1623                         );
1624                 }
1625
1626                 public class ReloadPlugin {
1627
1628                         @Test
1629                         public void reloadingPluginWorks() throws InterruptedException, ExecutionException, IOException {
1630                                 Future<Optional<PluginInfo>> pluginInfo = fcpClient.reloadPlugin().plugin(CLASS_NAME).execute();
1631                                 connectAndAssert(() -> matchReloadPluginMessage());
1632                                 replyWithPluginInfo();
1633                                 verifyPluginInfo(pluginInfo);
1634                         }
1635
1636                         @Test
1637                         public void reloadingPluginWithMaxWaitTimeWorks()
1638                         throws InterruptedException, ExecutionException, IOException {
1639                                 Future<Optional<PluginInfo>> pluginInfo =
1640                                         fcpClient.reloadPlugin().waitFor(1234).plugin(CLASS_NAME).execute();
1641                                 connectAndAssert(() -> allOf(matchReloadPluginMessage(), hasItem("MaxWaitTime=1234")));
1642                                 replyWithPluginInfo();
1643                                 verifyPluginInfo(pluginInfo);
1644                         }
1645
1646                         @Test
1647                         public void reloadingPluginWithPurgeWorks()
1648                         throws InterruptedException, ExecutionException, IOException {
1649                                 Future<Optional<PluginInfo>> pluginInfo =
1650                                         fcpClient.reloadPlugin().purge().plugin(CLASS_NAME).execute();
1651                                 connectAndAssert(() -> allOf(matchReloadPluginMessage(), hasItem("Purge=true")));
1652                                 replyWithPluginInfo();
1653                                 verifyPluginInfo(pluginInfo);
1654                         }
1655
1656                         @Test
1657                         public void reloadingPluginWithStoreWorks()
1658                         throws InterruptedException, ExecutionException, IOException {
1659                                 Future<Optional<PluginInfo>> pluginInfo =
1660                                         fcpClient.reloadPlugin().addToConfig().plugin(CLASS_NAME).execute();
1661                                 connectAndAssert(() -> allOf(matchReloadPluginMessage(), hasItem("Store=true")));
1662                                 replyWithPluginInfo();
1663                                 verifyPluginInfo(pluginInfo);
1664                         }
1665
1666                         private Matcher<List<String>> matchReloadPluginMessage() {
1667                                 return matchesFcpMessage(
1668                                         "ReloadPlugin",
1669                                         "Identifier=" + identifier,
1670                                         "PluginName=" + CLASS_NAME
1671                                 );
1672                         }
1673
1674                 }
1675
1676                 public class RemovePlugin {
1677
1678                         @Test
1679                         public void removingPluginWorks() throws InterruptedException, ExecutionException, IOException {
1680                                 Future<Boolean> pluginRemoved = fcpClient.removePlugin().plugin(CLASS_NAME).execute();
1681                                 connectAndAssert(() -> matchPluginRemovedMessage());
1682                                 replyWithPluginRemoved();
1683                                 assertThat(pluginRemoved.get(), is(true));
1684                         }
1685
1686                         @Test
1687                         public void removingPluginWithMaxWaitTimeWorks()
1688                         throws InterruptedException, ExecutionException, IOException {
1689                                 Future<Boolean> pluginRemoved = fcpClient.removePlugin().waitFor(1234).plugin(CLASS_NAME).execute();
1690                                 connectAndAssert(() -> allOf(matchPluginRemovedMessage(), hasItem("MaxWaitTime=1234")));
1691                                 replyWithPluginRemoved();
1692                                 assertThat(pluginRemoved.get(), is(true));
1693                         }
1694
1695                         @Test
1696                         public void removingPluginWithPurgeWorks()
1697                         throws InterruptedException, ExecutionException, IOException {
1698                                 Future<Boolean> pluginRemoved = fcpClient.removePlugin().purge().plugin(CLASS_NAME).execute();
1699                                 connectAndAssert(() -> allOf(matchPluginRemovedMessage(), hasItem("Purge=true")));
1700                                 replyWithPluginRemoved();
1701                                 assertThat(pluginRemoved.get(), is(true));
1702                         }
1703
1704                         private void replyWithPluginRemoved() throws IOException {
1705                                 fcpServer.writeLine(
1706                                         "PluginRemoved",
1707                                         "Identifier=" + identifier,
1708                                         "PluginName=" + CLASS_NAME,
1709                                         "EndMessage"
1710                                 );
1711                         }
1712
1713                         private Matcher<List<String>> matchPluginRemovedMessage() {
1714                                 return matchesFcpMessage(
1715                                         "RemovePlugin",
1716                                         "Identifier=" + identifier,
1717                                         "PluginName=" + CLASS_NAME
1718                                 );
1719                         }
1720
1721                 }
1722
1723                 public class GetPluginInfo {
1724
1725                         @Test
1726                         public void gettingPluginInfoWorks() throws InterruptedException, ExecutionException, IOException {
1727                                 Future<Optional<PluginInfo>> pluginInfo = fcpClient.getPluginInfo().plugin(CLASS_NAME).execute();
1728                                 connectAndAssert(() -> matchGetPluginInfoMessage());
1729                                 replyWithPluginInfo();
1730                                 verifyPluginInfo(pluginInfo);
1731                         }
1732
1733                         @Test
1734                         public void gettingPluginInfoWithDetailsWorks()
1735                         throws InterruptedException, ExecutionException, IOException {
1736                                 Future<Optional<PluginInfo>> pluginInfo =
1737                                         fcpClient.getPluginInfo().detailed().plugin(CLASS_NAME).execute();
1738                                 connectAndAssert(() -> allOf(matchGetPluginInfoMessage(), hasItem("Detailed=true")));
1739                                 replyWithPluginInfo();
1740                                 verifyPluginInfo(pluginInfo);
1741                         }
1742
1743                         @Test
1744                         public void protocolErrorIsRecognizedAsFailure()
1745                         throws InterruptedException, ExecutionException, IOException {
1746                                 Future<Optional<PluginInfo>> pluginInfo =
1747                                         fcpClient.getPluginInfo().detailed().plugin(CLASS_NAME).execute();
1748                                 connectAndAssert(() -> allOf(matchGetPluginInfoMessage(), hasItem("Detailed=true")));
1749                                 replyWithProtocolError();
1750                                 assertThat(pluginInfo.get(), is(Optional.empty()));
1751                         }
1752
1753                         private Matcher<List<String>> matchGetPluginInfoMessage() {
1754                                 return matchesFcpMessage(
1755                                         "GetPluginInfo",
1756                                         "Identifier=" + identifier,
1757                                         "PluginName=" + CLASS_NAME
1758                                 );
1759                         }
1760
1761                 }
1762
1763         }
1764
1765         public class UskSubscriptionCommands {
1766
1767                 private static final String URI = "USK@some,uri/file.txt";
1768
1769                 @Test
1770                 public void subscriptionWorks() throws InterruptedException, ExecutionException, IOException {
1771                         Future<Optional<UskSubscription>> uskSubscription = fcpClient.subscribeUsk().uri(URI).execute();
1772                         connectAndAssert(() -> matchesFcpMessage("SubscribeUSK", "URI=" + URI));
1773                         replyWithSubscribed();
1774                         assertThat(uskSubscription.get().get().getUri(), is(URI));
1775                         AtomicInteger edition = new AtomicInteger();
1776                         CountDownLatch updated = new CountDownLatch(2);
1777                         uskSubscription.get().get().onUpdate(e -> {
1778                                 edition.set(e);
1779                                 updated.countDown();
1780                         });
1781                         sendUpdateNotification(23);
1782                         sendUpdateNotification(24);
1783                         assertThat("updated in time", updated.await(5, TimeUnit.SECONDS), is(true));
1784                         assertThat(edition.get(), is(24));
1785                 }
1786
1787                 @Test
1788                 public void subscriptionUpdatesMultipleTimes() throws InterruptedException, ExecutionException, IOException {
1789                         Future<Optional<UskSubscription>> uskSubscription = fcpClient.subscribeUsk().uri(URI).execute();
1790                         connectAndAssert(() -> matchesFcpMessage("SubscribeUSK", "URI=" + URI));
1791                         replyWithSubscribed();
1792                         assertThat(uskSubscription.get().get().getUri(), is(URI));
1793                         AtomicInteger edition = new AtomicInteger();
1794                         CountDownLatch updated = new CountDownLatch(2);
1795                         uskSubscription.get().get().onUpdate(e -> {
1796                                 edition.set(e);
1797                                 updated.countDown();
1798                         });
1799                         uskSubscription.get().get().onUpdate(e -> updated.countDown());
1800                         sendUpdateNotification(23);
1801                         assertThat("updated in time", updated.await(5, TimeUnit.SECONDS), is(true));
1802                         assertThat(edition.get(), is(23));
1803                 }
1804
1805                 @Test
1806                 public void subscriptionCanBeCancelled() throws InterruptedException, ExecutionException, IOException {
1807                         Future<Optional<UskSubscription>> uskSubscription = fcpClient.subscribeUsk().uri(URI).execute();
1808                         connectAndAssert(() -> matchesFcpMessage("SubscribeUSK", "URI=" + URI));
1809                         replyWithSubscribed();
1810                         assertThat(uskSubscription.get().get().getUri(), is(URI));
1811                         AtomicBoolean updated = new AtomicBoolean();
1812                         uskSubscription.get().get().onUpdate(e -> updated.set(true));
1813                         uskSubscription.get().get().cancel();
1814                         readMessage(() -> matchesFcpMessage("UnsubscribeUSK", "Identifier=" + identifier));
1815                         sendUpdateNotification(23);
1816                         assertThat(updated.get(), is(false));
1817                 }
1818
1819                 private void replyWithSubscribed() throws IOException {
1820                         fcpServer.writeLine(
1821                                 "SubscribedUSK",
1822                                 "Identifier=" + identifier,
1823                                 "URI=" + URI,
1824                                 "DontPoll=false",
1825                                 "EndMessage"
1826                         );
1827                 }
1828
1829                 private void sendUpdateNotification(int edition, String... additionalLines) throws IOException {
1830                         fcpServer.writeLine(
1831                                 "SubscribedUSKUpdate",
1832                                 "Identifier=" + identifier,
1833                                 "URI=" + URI,
1834                                 "Edition=" + edition
1835                         );
1836                         fcpServer.writeLine(additionalLines);
1837                         fcpServer.writeLine("EndMessage");
1838                 }
1839
1840         }
1841
1842         public class ClientGet {
1843
1844                 @Test
1845                 public void works() throws InterruptedException, ExecutionException, IOException {
1846                         Future<Optional<Data>> dataFuture = fcpClient.clientGet().uri("KSK@foo.txt").execute();
1847                         connectAndAssert(() -> matchesFcpMessage("ClientGet", "URI=KSK@foo.txt", "ReturnType=direct"));
1848                         replyWithAllData("not-test", "Hello World", "text/plain;charset=latin-9");
1849                         replyWithAllData(identifier, "Hello", "text/plain;charset=utf-8");
1850                         Optional<Data> data = dataFuture.get();
1851                         verifyData(data);
1852                 }
1853
1854                 @Test
1855                 public void getFailedIsRecognized() throws InterruptedException, ExecutionException, IOException {
1856                         Future<Optional<Data>> dataFuture = fcpClient.clientGet().uri("KSK@foo.txt").execute();
1857                         connectAndAssert(() -> matchesFcpMessage("ClientGet", "URI=KSK@foo.txt"));
1858                         replyWithGetFailed("not-test");
1859                         replyWithGetFailed(identifier);
1860                         Optional<Data> data = dataFuture.get();
1861                         assertThat(data.isPresent(), is(false));
1862                 }
1863
1864                 @Test
1865                 public void getFailedForDifferentIdentifierIsIgnored()
1866                 throws InterruptedException, ExecutionException, IOException {
1867                         Future<Optional<Data>> dataFuture = fcpClient.clientGet().uri("KSK@foo.txt").execute();
1868                         connectAndAssert(() -> matchesFcpMessage("ClientGet", "URI=KSK@foo.txt"));
1869                         replyWithGetFailed("not-test");
1870                         replyWithAllData(identifier, "Hello", "text/plain;charset=utf-8");
1871                         Optional<Data> data = dataFuture.get();
1872                         verifyData(data);
1873                 }
1874
1875                 @Test(expected = ExecutionException.class)
1876                 public void connectionClosedIsRecognized() throws InterruptedException, ExecutionException, IOException {
1877                         Future<Optional<Data>> dataFuture = fcpClient.clientGet().uri("KSK@foo.txt").execute();
1878                         connectAndAssert(() -> matchesFcpMessage("ClientGet", "URI=KSK@foo.txt"));
1879                         fcpServer.close();
1880                         dataFuture.get();
1881                 }
1882
1883                 @Test
1884                 public void withIgnoreData() throws InterruptedException, ExecutionException, IOException {
1885                         fcpClient.clientGet().ignoreDataStore().uri("KSK@foo.txt").execute();
1886                         connectAndAssert(() -> matchesFcpMessage("ClientGet", "URI=KSK@foo.txt", "IgnoreDS=true"));
1887                 }
1888
1889                 @Test
1890                 public void withDataStoreOnly() throws InterruptedException, ExecutionException, IOException {
1891                         fcpClient.clientGet().dataStoreOnly().uri("KSK@foo.txt").execute();
1892                         connectAndAssert(() -> matchesFcpMessage("ClientGet", "URI=KSK@foo.txt", "DSonly=true"));
1893                 }
1894
1895                 @Test
1896                 public void clientGetWithMaxSizeSettingSendsCorrectCommands()
1897                 throws InterruptedException, ExecutionException, IOException {
1898                         fcpClient.clientGet().maxSize(1048576).uri("KSK@foo.txt").execute();
1899                         connectAndAssert(() -> matchesFcpMessage("ClientGet", "URI=KSK@foo.txt", "MaxSize=1048576"));
1900                 }
1901
1902                 @Test
1903                 public void clientGetWithPrioritySettingSendsCorrectCommands()
1904                 throws InterruptedException, ExecutionException, IOException {
1905                         fcpClient.clientGet().priority(Priority.interactive).uri("KSK@foo.txt").execute();
1906                         connectAndAssert(() -> matchesFcpMessage("ClientGet", "URI=KSK@foo.txt", "PriorityClass=1"));
1907                 }
1908
1909                 @Test
1910                 public void clientGetWithRealTimeSettingSendsCorrectCommands()
1911                 throws InterruptedException, ExecutionException, IOException {
1912                         fcpClient.clientGet().realTime().uri("KSK@foo.txt").execute();
1913                         connectAndAssert(() -> matchesFcpMessage("ClientGet", "URI=KSK@foo.txt", "RealTimeFlag=true"));
1914                 }
1915
1916                 @Test
1917                 public void clientGetWithGlobalSettingSendsCorrectCommands()
1918                 throws InterruptedException, ExecutionException, IOException {
1919                         fcpClient.clientGet().global().uri("KSK@foo.txt").execute();
1920                         connectAndAssert(() -> matchesFcpMessage("ClientGet", "URI=KSK@foo.txt", "Global=true"));
1921                 }
1922
1923                 private void replyWithGetFailed(String identifier) throws IOException {
1924                         fcpServer.writeLine(
1925                                 "GetFailed",
1926                                 "Identifier=" + identifier,
1927                                 "Code=3",
1928                                 "EndMessage"
1929                         );
1930                 }
1931
1932                 private void replyWithAllData(String identifier, String text, String contentType) throws IOException {
1933                         fcpServer.writeLine(
1934                                 "AllData",
1935                                 "Identifier=" + identifier,
1936                                 "DataLength=" + (text.length() + 1),
1937                                 "StartupTime=1435610539000",
1938                                 "CompletionTime=1435610540000",
1939                                 "Metadata.ContentType=" + contentType,
1940                                 "Data",
1941                                 text
1942                         );
1943                 }
1944
1945                 private void verifyData(Optional<Data> data) throws IOException {
1946                         assertThat(data.get().getMimeType(), is("text/plain;charset=utf-8"));
1947                         assertThat(data.get().size(), is(6L));
1948                         assertThat(ByteStreams.toByteArray(data.get().getInputStream()),
1949                                 is("Hello\n".getBytes(StandardCharsets.UTF_8)));
1950                 }
1951
1952         }
1953
1954 }