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