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