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