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