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