1 package net.pterodactylus.fcp.quelaton;
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;
14 import java.io.ByteArrayInputStream;
16 import java.io.IOException;
18 import java.nio.charset.StandardCharsets;
19 import java.util.Arrays;
20 import java.util.Collection;
21 import java.util.List;
22 import java.util.Optional;
23 import java.util.concurrent.CopyOnWriteArrayList;
24 import java.util.concurrent.CountDownLatch;
25 import java.util.concurrent.ExecutionException;
26 import java.util.concurrent.ExecutorService;
27 import java.util.concurrent.Executors;
28 import java.util.concurrent.Future;
29 import java.util.concurrent.TimeUnit;
30 import java.util.concurrent.atomic.AtomicBoolean;
31 import java.util.concurrent.atomic.AtomicInteger;
32 import java.util.function.Supplier;
33 import java.util.stream.Collectors;
35 import net.pterodactylus.fcp.ARK;
36 import net.pterodactylus.fcp.ConfigData;
37 import net.pterodactylus.fcp.DSAGroup;
38 import net.pterodactylus.fcp.FcpKeyPair;
39 import net.pterodactylus.fcp.Key;
40 import net.pterodactylus.fcp.NodeData;
41 import net.pterodactylus.fcp.NodeRef;
42 import net.pterodactylus.fcp.Peer;
43 import net.pterodactylus.fcp.PeerNote;
44 import net.pterodactylus.fcp.PluginInfo;
45 import net.pterodactylus.fcp.Priority;
46 import net.pterodactylus.fcp.fake.FakeTcpServer;
47 import net.pterodactylus.fcp.quelaton.ClientGetCommand.Data;
49 import com.google.common.io.ByteStreams;
50 import com.google.common.io.Files;
51 import com.nitorcreations.junit.runners.NestedRunner;
52 import org.hamcrest.Description;
53 import org.hamcrest.Matcher;
54 import org.hamcrest.Matchers;
55 import org.hamcrest.TypeSafeDiagnosingMatcher;
56 import org.junit.After;
57 import org.junit.Assert;
58 import org.junit.Test;
59 import org.junit.runner.RunWith;
62 * Unit test for {@link DefaultFcpClient}.
64 * @author <a href="bombe@freenetproject.org">David ‘Bombe’ Roden</a>
66 @RunWith(NestedRunner.class)
67 public class DefaultFcpClientTest {
69 private static final String INSERT_URI =
70 "SSK@RVCHbJdkkyTCeNN9AYukEg76eyqmiosSaNKgE3U9zUw,7SHH53gletBVb9JD7nBsyClbLQsBubDPEIcwg908r7Y,AQECAAE/";
71 private static final String REQUEST_URI =
72 "SSK@wtbgd2loNcJCXvtQVOftl2tuWBomDQHfqS6ytpPRhfw,7SHH53gletBVb9JD7nBsyClbLQsBubDPEIcwg908r7Y,AQACAAE/";
74 private int threadCounter = 0;
75 private final ExecutorService threadPool =
76 Executors.newCachedThreadPool(r -> new Thread(r, "Test-Thread-" + threadCounter++));
77 private final FakeTcpServer fcpServer;
78 private final DefaultFcpClient fcpClient;
80 public DefaultFcpClientTest() throws IOException {
81 fcpServer = new FakeTcpServer(threadPool);
82 fcpClient = new DefaultFcpClient(threadPool, "localhost", fcpServer.getPort(), () -> "Test");
86 public void tearDown() throws IOException {
88 threadPool.shutdown();
91 private void connectNode() throws InterruptedException, ExecutionException, IOException {
92 fcpServer.connect().get();
93 fcpServer.collectUntil(is("EndMessage"));
94 fcpServer.writeLine("NodeHello",
95 "CompressionCodecs=4 - GZIP(0), BZIP2(1), LZMA(2), LZMA_NEW(3)",
96 "Revision=build01466",
98 "Version=Fred,0.7,1.0,1466",
100 "ConnectionIdentifier=14318898267048452a81b36e7f13a3f0",
104 "NodeLanguage=ENGLISH",
110 private String extractIdentifier(List<String> lines) {
111 return lines.stream()
112 .filter(s -> s.startsWith("Identifier="))
113 .map(s -> s.substring(s.indexOf('=') + 1))
118 private Matcher<List<String>> matchesFcpMessage(String name, String... requiredLines) {
119 return matchesFcpMessageWithTerminator(name, "EndMessage", requiredLines);
122 private Matcher<Iterable<String>> hasHead(String firstElement) {
123 return new TypeSafeDiagnosingMatcher<Iterable<String>>() {
125 protected boolean matchesSafely(Iterable<String> iterable, Description mismatchDescription) {
126 if (!iterable.iterator().hasNext()) {
127 mismatchDescription.appendText("is empty");
130 String element = iterable.iterator().next();
131 if (!element.equals(firstElement)) {
132 mismatchDescription.appendText("starts with ").appendValue(element);
139 public void describeTo(Description description) {
140 description.appendText("starts with ").appendValue(firstElement);
145 private Matcher<List<String>> matchesFcpMessageWithTerminator(
146 String name, String terminator, String... requiredLines) {
147 return allOf(hasHead(name), hasParameters(1, 1, requiredLines), hasTail(terminator));
150 private Matcher<List<String>> hasParameters(int ignoreStart, int ignoreEnd, String... lines) {
151 return new TypeSafeDiagnosingMatcher<List<String>>() {
153 protected boolean matchesSafely(List<String> item, Description mismatchDescription) {
154 if (item.size() < (ignoreStart + ignoreEnd)) {
155 mismatchDescription.appendText("has only ").appendValue(item.size()).appendText(" elements");
158 for (String line : lines) {
159 if ((item.indexOf(line) < ignoreStart) || (item.indexOf(line) >= (item.size() - ignoreEnd))) {
160 mismatchDescription.appendText("does not contains ").appendValue(line);
168 public void describeTo(Description description) {
169 description.appendText("contains ").appendValueList("(", ", ", ")", lines);
170 description.appendText(", ignoring the first ").appendValue(ignoreStart);
171 description.appendText(" and the last ").appendValue(ignoreEnd);
176 private Matcher<List<String>> hasTail(String... lastElements) {
177 return new TypeSafeDiagnosingMatcher<List<String>>() {
179 protected boolean matchesSafely(List<String> list, Description mismatchDescription) {
180 if (list.size() < lastElements.length) {
181 mismatchDescription.appendText("is too small");
184 List<String> tail = list.subList(list.size() - lastElements.length, list.size());
185 if (!tail.equals(Arrays.asList(lastElements))) {
186 mismatchDescription.appendText("ends with ").appendValueList("(", ", ", ")", tail);
193 public void describeTo(Description description) {
194 description.appendText("ends with ").appendValueList("(", ", ", ")", lastElements);
199 private List<String> lines;
200 private String identifier;
202 private void connectAndAssert(Supplier<Matcher<List<String>>> requestMatcher)
203 throws InterruptedException, ExecutionException, IOException {
205 readMessage(requestMatcher);
208 private void readMessage(Supplier<Matcher<List<String>>> requestMatcher) throws IOException {
209 readMessage("EndMessage", requestMatcher);
212 private void readMessage(String terminator, Supplier<Matcher<List<String>>> requestMatcher) throws IOException {
213 lines = fcpServer.collectUntil(is(terminator));
214 identifier = extractIdentifier(lines);
215 assertThat(lines, requestMatcher.get());
218 public class ConnectionsAndKeyPairs {
220 public class Connections {
222 @Test(expected = ExecutionException.class)
223 public void throwsExceptionOnFailure() throws IOException, ExecutionException, InterruptedException {
224 Future<FcpKeyPair> keyPairFuture = fcpClient.generateKeypair().execute();
225 connectAndAssert(() -> matchesFcpMessage("GenerateSSK"));
227 "CloseConnectionDuplicateClientName",
233 @Test(expected = ExecutionException.class)
234 public void throwsExceptionIfConnectionIsClosed() throws IOException, ExecutionException, InterruptedException {
235 Future<FcpKeyPair> keyPairFuture = fcpClient.generateKeypair().execute();
236 connectAndAssert(() -> matchesFcpMessage("GenerateSSK"));
242 public void connectionIsReused() throws InterruptedException, ExecutionException, IOException {
243 Future<FcpKeyPair> keyPair = fcpClient.generateKeypair().execute();
244 connectAndAssert(() -> matchesFcpMessage("GenerateSSK"));
247 keyPair = fcpClient.generateKeypair().execute();
248 readMessage(() -> matchesFcpMessage("GenerateSSK"));
249 identifier = extractIdentifier(lines);
255 public void defaultFcpClientCanReconnectAfterConnectionHasBeenClosed()
256 throws InterruptedException, ExecutionException, IOException {
257 Future<FcpKeyPair> keyPair = fcpClient.generateKeypair().execute();
258 connectAndAssert(() -> matchesFcpMessage("GenerateSSK"));
263 } catch (ExecutionException e) {
266 keyPair = fcpClient.generateKeypair().execute();
267 connectAndAssert(() -> matchesFcpMessage("GenerateSSK"));
274 public class GenerateKeyPair {
277 public void defaultFcpClientCanGenerateKeypair()
278 throws ExecutionException, InterruptedException, IOException {
279 Future<FcpKeyPair> keyPairFuture = fcpClient.generateKeypair().execute();
280 connectAndAssert(() -> matchesFcpMessage("GenerateSSK"));
282 FcpKeyPair keyPair = keyPairFuture.get();
283 assertThat(keyPair.getPublicKey(), is(REQUEST_URI));
284 assertThat(keyPair.getPrivateKey(), is(INSERT_URI));
289 private void replyWithKeyPair() throws IOException {
290 fcpServer.writeLine("SSKKeypair",
291 "InsertURI=" + INSERT_URI + "",
292 "RequestURI=" + REQUEST_URI + "",
293 "Identifier=" + identifier,
301 public class PeerCommands {
303 public class ListPeer {
306 public void byIdentity() throws InterruptedException, ExecutionException, IOException {
307 Future<Optional<Peer>> peer = fcpClient.listPeer().byIdentity("id1").execute();
308 connectAndAssert(() -> matchesListPeer("id1"));
309 replyWithPeer("id1");
310 assertThat(peer.get().get().getIdentity(), is("id1"));
314 public void byHostAndPort() throws InterruptedException, ExecutionException, IOException {
315 Future<Optional<Peer>> peer = fcpClient.listPeer().byHostAndPort("host.free.net", 12345).execute();
316 connectAndAssert(() -> matchesListPeer("host.free.net:12345"));
317 replyWithPeer("id1");
318 assertThat(peer.get().get().getIdentity(), is("id1"));
322 public void byName() throws InterruptedException, ExecutionException, IOException {
323 Future<Optional<Peer>> peer = fcpClient.listPeer().byName("FriendNode").execute();
324 connectAndAssert(() -> matchesListPeer("FriendNode"));
325 replyWithPeer("id1");
326 assertThat(peer.get().get().getIdentity(), is("id1"));
330 public void unknownNodeIdentifier() throws InterruptedException, ExecutionException, IOException {
331 Future<Optional<Peer>> peer = fcpClient.listPeer().byIdentity("id2").execute();
332 connectAndAssert(() -> matchesListPeer("id2"));
333 replyWithUnknownNodeIdentifier();
334 assertThat(peer.get().isPresent(), is(false));
337 private Matcher<List<String>> matchesListPeer(String nodeId) {
338 return matchesFcpMessage(
340 "Identifier=" + identifier,
341 "NodeIdentifier=" + nodeId
347 public class ListPeers {
350 public void withoutMetadataOrVolatile() throws IOException, ExecutionException, InterruptedException {
351 Future<Collection<Peer>> peers = fcpClient.listPeers().execute();
352 connectAndAssert(() -> matchesListPeers(false, false));
353 replyWithPeer("id1");
354 replyWithPeer("id2");
356 assertThat(peers.get(), hasSize(2));
357 assertThat(peers.get().stream().map(Peer::getIdentity).collect(Collectors.toList()),
358 containsInAnyOrder("id1", "id2"));
362 public void withMetadata() throws IOException, ExecutionException, InterruptedException {
363 Future<Collection<Peer>> peers = fcpClient.listPeers().includeMetadata().execute();
364 connectAndAssert(() -> matchesListPeers(false, true));
365 replyWithPeer("id1", "metadata.foo=bar1");
366 replyWithPeer("id2", "metadata.foo=bar2");
368 assertThat(peers.get(), hasSize(2));
369 assertThat(peers.get().stream().map(peer -> peer.getMetadata("foo")).collect(Collectors.toList()),
370 containsInAnyOrder("bar1", "bar2"));
374 public void withVolatile() throws IOException, ExecutionException, InterruptedException {
375 Future<Collection<Peer>> peers = fcpClient.listPeers().includeVolatile().execute();
376 connectAndAssert(() -> matchesListPeers(true, false));
377 replyWithPeer("id1", "volatile.foo=bar1");
378 replyWithPeer("id2", "volatile.foo=bar2");
380 assertThat(peers.get(), hasSize(2));
381 assertThat(peers.get().stream().map(peer -> peer.getVolatile("foo")).collect(Collectors.toList()),
382 containsInAnyOrder("bar1", "bar2"));
385 private Matcher<List<String>> matchesListPeers(boolean withVolatile, boolean withMetadata) {
386 return matchesFcpMessage(
388 "WithVolatile=" + withVolatile,
389 "WithMetadata=" + withMetadata
393 private void sendEndOfPeerList() throws IOException {
396 "Identifier=" + identifier,
403 public class AddPeer {
406 public void fromFile() throws InterruptedException, ExecutionException, IOException {
407 Future<Optional<Peer>> peer = fcpClient.addPeer().fromFile(new File("/tmp/ref.txt")).execute();
408 connectAndAssert(() -> allOf(matchesAddPeer(), hasItem("File=/tmp/ref.txt")));
409 replyWithPeer("id1");
410 assertThat(peer.get().get().getIdentity(), is("id1"));
414 public void fromUrl() throws InterruptedException, ExecutionException, IOException {
415 Future<Optional<Peer>> peer = fcpClient.addPeer().fromURL(new URL("http://node.ref/")).execute();
416 connectAndAssert(() -> allOf(matchesAddPeer(), hasItem("URL=http://node.ref/")));
417 replyWithPeer("id1");
418 assertThat(peer.get().get().getIdentity(), is("id1"));
422 public void fromNodeRef() throws InterruptedException, ExecutionException, IOException {
423 NodeRef nodeRef = createNodeRef();
424 Future<Optional<Peer>> peer = fcpClient.addPeer().fromNodeRef(nodeRef).execute();
425 connectAndAssert(() -> allOf(matchesAddPeer(), Matchers.<String>hasItems(
431 "dsaGroup.q=subprime",
432 "dsaPubKey.y=dsa-public",
433 "physical.udp=1.2.3.4:5678",
437 replyWithPeer("id1");
438 assertThat(peer.get().get().getIdentity(), is("id1"));
441 private NodeRef createNodeRef() {
442 NodeRef nodeRef = new NodeRef();
443 nodeRef.setIdentity("id1");
444 nodeRef.setName("name");
445 nodeRef.setARK(new ARK("public", "1"));
446 nodeRef.setDSAGroup(new DSAGroup("base", "prime", "subprime"));
447 nodeRef.setNegotiationTypes(new int[] { 3, 5 });
448 nodeRef.setPhysicalUDP("1.2.3.4:5678");
449 nodeRef.setDSAPublicKey("dsa-public");
450 nodeRef.setSignature("sig");
454 private Matcher<List<String>> matchesAddPeer() {
455 return matchesFcpMessage(
457 "Identifier=" + identifier
463 public class ModifyPeer {
466 public void defaultFcpClientCanEnablePeerByName()
467 throws InterruptedException, ExecutionException, IOException {
468 Future<Optional<Peer>> peer = fcpClient.modifyPeer().enable().byName("id1").execute();
469 connectAndAssert(() -> matchesModifyPeer("id1", "IsDisabled", false));
470 replyWithPeer("id1");
471 assertThat(peer.get().get().getIdentity(), is("id1"));
475 public void defaultFcpClientCanDisablePeerByName()
476 throws InterruptedException, ExecutionException, IOException {
477 Future<Optional<Peer>> peer = fcpClient.modifyPeer().disable().byName("id1").execute();
478 connectAndAssert(() -> matchesModifyPeer("id1", "IsDisabled", true));
479 replyWithPeer("id1");
480 assertThat(peer.get().get().getIdentity(), is("id1"));
484 public void defaultFcpClientCanEnablePeerByIdentity()
485 throws InterruptedException, ExecutionException, IOException {
486 Future<Optional<Peer>> peer = fcpClient.modifyPeer().enable().byIdentity("id1").execute();
487 connectAndAssert(() -> matchesModifyPeer("id1", "IsDisabled", false));
488 replyWithPeer("id1");
489 assertThat(peer.get().get().getIdentity(), is("id1"));
493 public void defaultFcpClientCanEnablePeerByHostAndPort()
494 throws InterruptedException, ExecutionException, IOException {
495 Future<Optional<Peer>> peer =
496 fcpClient.modifyPeer().enable().byHostAndPort("1.2.3.4", 5678).execute();
497 connectAndAssert(() -> matchesModifyPeer("1.2.3.4:5678", "IsDisabled", false));
498 replyWithPeer("id1");
499 assertThat(peer.get().get().getIdentity(), is("id1"));
503 public void allowLocalAddressesOfPeer() throws InterruptedException, ExecutionException, IOException {
504 Future<Optional<Peer>> peer =
505 fcpClient.modifyPeer().allowLocalAddresses().byIdentity("id1").execute();
506 connectAndAssert(() -> allOf(
507 matchesModifyPeer("id1", "AllowLocalAddresses", true),
508 not(contains(startsWith("IsDisabled=")))
510 replyWithPeer("id1");
511 assertThat(peer.get().get().getIdentity(), is("id1"));
515 public void disallowLocalAddressesOfPeer()
516 throws InterruptedException, ExecutionException, IOException {
517 Future<Optional<Peer>> peer =
518 fcpClient.modifyPeer().disallowLocalAddresses().byIdentity("id1").execute();
519 connectAndAssert(() -> allOf(
520 matchesModifyPeer("id1", "AllowLocalAddresses", false),
521 not(contains(startsWith("IsDisabled=")))
523 replyWithPeer("id1");
524 assertThat(peer.get().get().getIdentity(), is("id1"));
528 public void setBurstOnlyForPeer() throws InterruptedException, ExecutionException, IOException {
529 Future<Optional<Peer>> peer = fcpClient.modifyPeer().setBurstOnly().byIdentity("id1").execute();
530 connectAndAssert(() -> allOf(
531 matchesModifyPeer("id1", "IsBurstOnly", true),
532 not(contains(startsWith("AllowLocalAddresses="))),
533 not(contains(startsWith("IsDisabled=")))
535 replyWithPeer("id1");
536 assertThat(peer.get().get().getIdentity(), is("id1"));
540 public void clearBurstOnlyForPeer() throws InterruptedException, ExecutionException, IOException {
541 Future<Optional<Peer>> peer = fcpClient.modifyPeer().clearBurstOnly().byIdentity("id1").execute();
542 connectAndAssert(() -> allOf(
543 matchesModifyPeer("id1", "IsBurstOnly", false),
544 not(contains(startsWith("AllowLocalAddresses="))),
545 not(contains(startsWith("IsDisabled=")))
547 replyWithPeer("id1");
548 assertThat(peer.get().get().getIdentity(), is("id1"));
552 public void defaultFcpClientCanSetListenOnlyForPeer()
553 throws InterruptedException, ExecutionException, IOException {
554 Future<Optional<Peer>> peer = fcpClient.modifyPeer().setListenOnly().byIdentity("id1").execute();
555 connectAndAssert(() -> allOf(
556 matchesModifyPeer("id1", "IsListenOnly", true),
557 not(contains(startsWith("AllowLocalAddresses="))),
558 not(contains(startsWith("IsDisabled="))),
559 not(contains(startsWith("IsBurstOnly=")))
561 replyWithPeer("id1");
562 assertThat(peer.get().get().getIdentity(), is("id1"));
566 public void clearListenOnlyForPeer() throws InterruptedException, ExecutionException, IOException {
567 Future<Optional<Peer>> peer = fcpClient.modifyPeer().clearListenOnly().byIdentity("id1").execute();
568 connectAndAssert(() -> allOf(
569 matchesModifyPeer("id1", "IsListenOnly", false),
570 not(contains(startsWith("AllowLocalAddresses="))),
571 not(contains(startsWith("IsDisabled="))),
572 not(contains(startsWith("IsBurstOnly=")))
574 replyWithPeer("id1");
575 assertThat(peer.get().get().getIdentity(), is("id1"));
579 public void ignoreSourceForPeer() throws InterruptedException, ExecutionException, IOException {
580 Future<Optional<Peer>> peer = fcpClient.modifyPeer().ignoreSource().byIdentity("id1").execute();
581 connectAndAssert(() -> allOf(
582 matchesModifyPeer("id1", "IgnoreSourcePort", true),
583 not(contains(startsWith("AllowLocalAddresses="))),
584 not(contains(startsWith("IsDisabled="))),
585 not(contains(startsWith("IsBurstOnly="))),
586 not(contains(startsWith("IsListenOnly=")))
588 replyWithPeer("id1");
589 assertThat(peer.get().get().getIdentity(), is("id1"));
593 public void useSourceForPeer() throws InterruptedException, ExecutionException, IOException {
594 Future<Optional<Peer>> peer = fcpClient.modifyPeer().useSource().byIdentity("id1").execute();
595 connectAndAssert(() -> allOf(
596 matchesModifyPeer("id1", "IgnoreSourcePort", false),
597 not(contains(startsWith("AllowLocalAddresses="))),
598 not(contains(startsWith("IsDisabled="))),
599 not(contains(startsWith("IsBurstOnly="))),
600 not(contains(startsWith("IsListenOnly=")))
602 replyWithPeer("id1");
603 assertThat(peer.get().get().getIdentity(), is("id1"));
607 public void unknownNode() throws InterruptedException, ExecutionException, IOException {
608 Future<Optional<Peer>> peer = fcpClient.modifyPeer().enable().byIdentity("id1").execute();
609 connectAndAssert(() -> matchesModifyPeer("id1", "IsDisabled", false));
610 replyWithUnknownNodeIdentifier();
611 assertThat(peer.get().isPresent(), is(false));
614 private Matcher<List<String>> matchesModifyPeer(String nodeIdentifier, String setting, boolean value) {
615 return matchesFcpMessage(
617 "Identifier=" + identifier,
618 "NodeIdentifier=" + nodeIdentifier,
619 setting + "=" + value
625 public class RemovePeer {
628 public void byName() throws InterruptedException, ExecutionException, IOException {
629 Future<Boolean> peer = fcpClient.removePeer().byName("Friend1").execute();
630 connectAndAssert(() -> matchesRemovePeer("Friend1"));
631 replyWithPeerRemoved("Friend1");
632 assertThat(peer.get(), is(true));
636 public void invalidName() throws InterruptedException, ExecutionException, IOException {
637 Future<Boolean> peer = fcpClient.removePeer().byName("NotFriend1").execute();
638 connectAndAssert(() -> matchesRemovePeer("NotFriend1"));
639 replyWithUnknownNodeIdentifier();
640 assertThat(peer.get(), is(false));
644 public void byIdentity() throws InterruptedException, ExecutionException, IOException {
645 Future<Boolean> peer = fcpClient.removePeer().byIdentity("id1").execute();
646 connectAndAssert(() -> matchesRemovePeer("id1"));
647 replyWithPeerRemoved("id1");
648 assertThat(peer.get(), is(true));
652 public void byHostAndPort() throws InterruptedException, ExecutionException, IOException {
653 Future<Boolean> peer = fcpClient.removePeer().byHostAndPort("1.2.3.4", 5678).execute();
654 connectAndAssert(() -> matchesRemovePeer("1.2.3.4:5678"));
655 replyWithPeerRemoved("Friend1");
656 assertThat(peer.get(), is(true));
659 private Matcher<List<String>> matchesRemovePeer(String nodeIdentifier) {
660 return matchesFcpMessage(
662 "Identifier=" + identifier,
663 "NodeIdentifier=" + nodeIdentifier
667 private void replyWithPeerRemoved(String nodeIdentifier) throws IOException {
670 "Identifier=" + identifier,
671 "NodeIdentifier=" + nodeIdentifier,
678 private void replyWithPeer(String peerId, String... additionalLines) throws IOException {
681 "Identifier=" + identifier,
682 "identity=" + peerId,
684 "ark.pubURI=SSK@3YEf.../ark",
687 "version=Fred,0.7,1.0,1466",
688 "lastGoodVersion=Fred,0.7,1.0,1466"
690 fcpServer.writeLine(additionalLines);
691 fcpServer.writeLine("EndMessage");
696 public class PeerNoteCommands {
698 public class ListPeerNotes {
701 public void onUnknownNodeIdentifier() throws InterruptedException, ExecutionException, IOException {
702 Future<Optional<PeerNote>> peerNote = fcpClient.listPeerNotes().byName("Friend1").execute();
703 connectAndAssert(() -> matchesListPeerNotes("Friend1"));
704 replyWithUnknownNodeIdentifier();
705 assertThat(peerNote.get().isPresent(), is(false));
709 public void byNodeName() throws InterruptedException, ExecutionException, IOException {
710 Future<Optional<PeerNote>> peerNote = fcpClient.listPeerNotes().byName("Friend1").execute();
711 connectAndAssert(() -> matchesListPeerNotes("Friend1"));
713 replyWithEndListPeerNotes();
714 assertThat(peerNote.get().get().getNoteText(), is("RXhhbXBsZSBUZXh0Lg=="));
715 assertThat(peerNote.get().get().getPeerNoteType(), is(1));
719 public void byNodeIdentifier() throws InterruptedException, ExecutionException, IOException {
720 Future<Optional<PeerNote>> peerNote = fcpClient.listPeerNotes().byIdentity("id1").execute();
721 connectAndAssert(() -> matchesListPeerNotes("id1"));
723 replyWithEndListPeerNotes();
724 assertThat(peerNote.get().get().getNoteText(), is("RXhhbXBsZSBUZXh0Lg=="));
725 assertThat(peerNote.get().get().getPeerNoteType(), is(1));
729 public void byHostAndPort() throws InterruptedException, ExecutionException, IOException {
730 Future<Optional<PeerNote>> peerNote =
731 fcpClient.listPeerNotes().byHostAndPort("1.2.3.4", 5678).execute();
732 connectAndAssert(() -> matchesListPeerNotes("1.2.3.4:5678"));
734 replyWithEndListPeerNotes();
735 assertThat(peerNote.get().get().getNoteText(), is("RXhhbXBsZSBUZXh0Lg=="));
736 assertThat(peerNote.get().get().getPeerNoteType(), is(1));
739 private Matcher<List<String>> matchesListPeerNotes(String nodeIdentifier) {
740 return matchesFcpMessage(
742 "NodeIdentifier=" + nodeIdentifier
746 private void replyWithEndListPeerNotes() throws IOException {
749 "Identifier=" + identifier,
754 private void replyWithPeerNote() throws IOException {
757 "Identifier=" + identifier,
758 "NodeIdentifier=Friend1",
759 "NoteText=RXhhbXBsZSBUZXh0Lg==",
767 public class ModifyPeerNotes {
770 public void byName() throws InterruptedException, ExecutionException, IOException {
771 Future<Boolean> noteUpdated =
772 fcpClient.modifyPeerNote().darknetComment("foo").byName("Friend1").execute();
773 connectAndAssert(() -> matchesModifyPeerNote("Friend1"));
775 assertThat(noteUpdated.get(), is(true));
779 public void onUnknownNodeIdentifier() throws InterruptedException, ExecutionException, IOException {
780 Future<Boolean> noteUpdated =
781 fcpClient.modifyPeerNote().darknetComment("foo").byName("Friend1").execute();
782 connectAndAssert(() -> matchesModifyPeerNote("Friend1"));
783 replyWithUnknownNodeIdentifier();
784 assertThat(noteUpdated.get(), is(false));
788 public void defaultFcpClientFailsToModifyPeerNoteWithoutPeerNote()
789 throws InterruptedException, ExecutionException, IOException {
790 Future<Boolean> noteUpdated = fcpClient.modifyPeerNote().byName("Friend1").execute();
791 assertThat(noteUpdated.get(), is(false));
795 public void byIdentifier() throws InterruptedException, ExecutionException, IOException {
796 Future<Boolean> noteUpdated =
797 fcpClient.modifyPeerNote().darknetComment("foo").byIdentifier("id1").execute();
798 connectAndAssert(() -> matchesModifyPeerNote("id1"));
800 assertThat(noteUpdated.get(), is(true));
804 public void byHostAndPort() throws InterruptedException, ExecutionException, IOException {
805 Future<Boolean> noteUpdated =
806 fcpClient.modifyPeerNote().darknetComment("foo").byHostAndPort("1.2.3.4", 5678).execute();
807 connectAndAssert(() -> matchesModifyPeerNote("1.2.3.4:5678"));
809 assertThat(noteUpdated.get(), is(true));
812 private Matcher<List<String>> matchesModifyPeerNote(String nodeIdentifier) {
813 return matchesFcpMessage(
815 "Identifier=" + identifier,
816 "NodeIdentifier=" + nodeIdentifier,
822 private void replyWithPeerNote() throws IOException {
825 "Identifier=" + identifier,
826 "NodeIdentifier=Friend1",
837 private void replyWithUnknownNodeIdentifier() throws IOException {
839 "UnknownNodeIdentifier",
840 "Identifier=" + identifier,
841 "NodeIdentifier=id2",
848 public class PluginCommands {
850 private static final String CLASS_NAME = "foo.plugin.Plugin";
852 private void replyWithPluginInfo() throws IOException {
855 "Identifier=" + identifier,
856 "PluginName=superPlugin",
860 "OriginUri=superPlugin",
866 private void verifyPluginInfo(Future<Optional<PluginInfo>> pluginInfo)
867 throws InterruptedException, ExecutionException {
868 assertThat(pluginInfo.get().get().getPluginName(), is("superPlugin"));
869 assertThat(pluginInfo.get().get().getOriginalURI(), is("superPlugin"));
870 assertThat(pluginInfo.get().get().isTalkable(), is(true));
871 assertThat(pluginInfo.get().get().getVersion(), is("42"));
872 assertThat(pluginInfo.get().get().getLongVersion(), is("1.2.3"));
873 assertThat(pluginInfo.get().get().isStarted(), is(true));
876 public class LoadPlugin {
878 public class OfficialPlugins {
881 public void fromFreenet() throws ExecutionException, InterruptedException, IOException {
882 Future<Optional<PluginInfo>> pluginInfo =
883 fcpClient.loadPlugin().officialFromFreenet("superPlugin").execute();
884 connectAndAssert(() -> createMatcherForOfficialSource("freenet"));
885 assertThat(lines, not(contains(startsWith("Store="))));
886 replyWithPluginInfo();
887 verifyPluginInfo(pluginInfo);
891 public void persistentFromFreenet() throws ExecutionException, InterruptedException, IOException {
892 Future<Optional<PluginInfo>> pluginInfo =
893 fcpClient.loadPlugin().addToConfig().officialFromFreenet("superPlugin").execute();
894 connectAndAssert(() -> createMatcherForOfficialSource("freenet"));
895 assertThat(lines, hasItem("Store=true"));
896 replyWithPluginInfo();
897 verifyPluginInfo(pluginInfo);
901 public void fromHttps() throws ExecutionException, InterruptedException, IOException {
902 Future<Optional<PluginInfo>> pluginInfo =
903 fcpClient.loadPlugin().officialFromHttps("superPlugin").execute();
904 connectAndAssert(() -> createMatcherForOfficialSource("https"));
905 replyWithPluginInfo();
906 verifyPluginInfo(pluginInfo);
909 private Matcher<List<String>> createMatcherForOfficialSource(String officialSource) {
910 return matchesFcpMessage(
912 "Identifier=" + identifier,
913 "PluginURL=superPlugin",
915 "OfficialSource=" + officialSource
921 public class FromOtherSources {
923 private static final String FILE_PATH = "/path/to/plugin.jar";
924 private static final String URL = "http://server.com/plugin.jar";
925 private static final String KEY = "KSK@plugin.jar";
928 public void fromFile() throws ExecutionException, InterruptedException, IOException {
929 Future<Optional<PluginInfo>> pluginInfo = fcpClient.loadPlugin().fromFile(FILE_PATH).execute();
930 connectAndAssert(() -> createMatcher("file", FILE_PATH));
931 replyWithPluginInfo();
932 verifyPluginInfo(pluginInfo);
936 public void fromUrl() throws ExecutionException, InterruptedException, IOException {
937 Future<Optional<PluginInfo>> pluginInfo = fcpClient.loadPlugin().fromUrl(URL).execute();
938 connectAndAssert(() -> createMatcher("url", URL));
939 replyWithPluginInfo();
940 verifyPluginInfo(pluginInfo);
944 public void fromFreenet() throws ExecutionException, InterruptedException, IOException {
945 Future<Optional<PluginInfo>> pluginInfo = fcpClient.loadPlugin().fromFreenet(KEY).execute();
946 connectAndAssert(() -> createMatcher("freenet", KEY));
947 replyWithPluginInfo();
948 verifyPluginInfo(pluginInfo);
951 private Matcher<List<String>> createMatcher(String urlType, String url) {
952 return matchesFcpMessage(
954 "Identifier=" + identifier,
962 public class Failed {
965 public void failedLoad() throws ExecutionException, InterruptedException, IOException {
966 Future<Optional<PluginInfo>> pluginInfo =
967 fcpClient.loadPlugin().officialFromFreenet("superPlugin").execute();
968 connectAndAssert(() -> matchesFcpMessage("LoadPlugin"));
969 replyWithProtocolError();
970 assertThat(pluginInfo.get().isPresent(), is(false));
977 private void replyWithProtocolError() throws IOException {
980 "Identifier=" + identifier,
985 public class ReloadPlugin {
988 public void reloadingPluginWorks() throws InterruptedException, ExecutionException, IOException {
989 Future<Optional<PluginInfo>> pluginInfo = fcpClient.reloadPlugin().plugin(CLASS_NAME).execute();
990 connectAndAssert(this::matchReloadPluginMessage);
991 replyWithPluginInfo();
992 verifyPluginInfo(pluginInfo);
996 public void reloadingPluginWithMaxWaitTimeWorks()
997 throws InterruptedException, ExecutionException, IOException {
998 Future<Optional<PluginInfo>> pluginInfo =
999 fcpClient.reloadPlugin().waitFor(1234).plugin(CLASS_NAME).execute();
1000 connectAndAssert(() -> allOf(matchReloadPluginMessage(), hasItem("MaxWaitTime=1234")));
1001 replyWithPluginInfo();
1002 verifyPluginInfo(pluginInfo);
1006 public void reloadingPluginWithPurgeWorks()
1007 throws InterruptedException, ExecutionException, IOException {
1008 Future<Optional<PluginInfo>> pluginInfo =
1009 fcpClient.reloadPlugin().purge().plugin(CLASS_NAME).execute();
1010 connectAndAssert(() -> allOf(matchReloadPluginMessage(), hasItem("Purge=true")));
1011 replyWithPluginInfo();
1012 verifyPluginInfo(pluginInfo);
1016 public void reloadingPluginWithStoreWorks()
1017 throws InterruptedException, ExecutionException, IOException {
1018 Future<Optional<PluginInfo>> pluginInfo =
1019 fcpClient.reloadPlugin().addToConfig().plugin(CLASS_NAME).execute();
1020 connectAndAssert(() -> allOf(matchReloadPluginMessage(), hasItem("Store=true")));
1021 replyWithPluginInfo();
1022 verifyPluginInfo(pluginInfo);
1025 private Matcher<List<String>> matchReloadPluginMessage() {
1026 return matchesFcpMessage(
1028 "Identifier=" + identifier,
1029 "PluginName=" + CLASS_NAME
1035 public class RemovePlugin {
1038 public void removingPluginWorks() throws InterruptedException, ExecutionException, IOException {
1039 Future<Boolean> pluginRemoved = fcpClient.removePlugin().plugin(CLASS_NAME).execute();
1040 connectAndAssert(this::matchPluginRemovedMessage);
1041 replyWithPluginRemoved();
1042 assertThat(pluginRemoved.get(), is(true));
1046 public void removingPluginWithMaxWaitTimeWorks()
1047 throws InterruptedException, ExecutionException, IOException {
1048 Future<Boolean> pluginRemoved = fcpClient.removePlugin().waitFor(1234).plugin(CLASS_NAME).execute();
1049 connectAndAssert(() -> allOf(matchPluginRemovedMessage(), hasItem("MaxWaitTime=1234")));
1050 replyWithPluginRemoved();
1051 assertThat(pluginRemoved.get(), is(true));
1055 public void removingPluginWithPurgeWorks()
1056 throws InterruptedException, ExecutionException, IOException {
1057 Future<Boolean> pluginRemoved = fcpClient.removePlugin().purge().plugin(CLASS_NAME).execute();
1058 connectAndAssert(() -> allOf(matchPluginRemovedMessage(), hasItem("Purge=true")));
1059 replyWithPluginRemoved();
1060 assertThat(pluginRemoved.get(), is(true));
1063 private void replyWithPluginRemoved() throws IOException {
1064 fcpServer.writeLine(
1066 "Identifier=" + identifier,
1067 "PluginName=" + CLASS_NAME,
1072 private Matcher<List<String>> matchPluginRemovedMessage() {
1073 return matchesFcpMessage(
1075 "Identifier=" + identifier,
1076 "PluginName=" + CLASS_NAME
1082 public class GetPluginInfo {
1085 public void gettingPluginInfoWorks() throws InterruptedException, ExecutionException, IOException {
1086 Future<Optional<PluginInfo>> pluginInfo = fcpClient.getPluginInfo().plugin(CLASS_NAME).execute();
1087 connectAndAssert(this::matchGetPluginInfoMessage);
1088 replyWithPluginInfo();
1089 verifyPluginInfo(pluginInfo);
1093 public void gettingPluginInfoWithDetailsWorks()
1094 throws InterruptedException, ExecutionException, IOException {
1095 Future<Optional<PluginInfo>> pluginInfo =
1096 fcpClient.getPluginInfo().detailed().plugin(CLASS_NAME).execute();
1097 connectAndAssert(() -> allOf(matchGetPluginInfoMessage(), hasItem("Detailed=true")));
1098 replyWithPluginInfo();
1099 verifyPluginInfo(pluginInfo);
1103 public void protocolErrorIsRecognizedAsFailure()
1104 throws InterruptedException, ExecutionException, IOException {
1105 Future<Optional<PluginInfo>> pluginInfo =
1106 fcpClient.getPluginInfo().detailed().plugin(CLASS_NAME).execute();
1107 connectAndAssert(() -> allOf(matchGetPluginInfoMessage(), hasItem("Detailed=true")));
1108 replyWithProtocolError();
1109 assertThat(pluginInfo.get(), is(Optional.empty()));
1112 private Matcher<List<String>> matchGetPluginInfoMessage() {
1113 return matchesFcpMessage(
1115 "Identifier=" + identifier,
1116 "PluginName=" + CLASS_NAME
1124 public class UskSubscriptionCommands {
1126 private static final String URI = "USK@some,uri/file.txt";
1129 public void subscriptionWorks() throws InterruptedException, ExecutionException, IOException {
1130 Future<Optional<UskSubscription>> uskSubscription = fcpClient.subscribeUsk().uri(URI).execute();
1131 connectAndAssert(() -> matchesFcpMessage("SubscribeUSK", "URI=" + URI));
1132 replyWithSubscribed();
1133 assertThat(uskSubscription.get().get().getUri(), is(URI));
1134 AtomicInteger edition = new AtomicInteger();
1135 CountDownLatch updated = new CountDownLatch(2);
1136 uskSubscription.get().get().onUpdate(e -> {
1138 updated.countDown();
1140 sendUpdateNotification(23);
1141 sendUpdateNotification(24);
1142 assertThat("updated in time", updated.await(5, TimeUnit.SECONDS), is(true));
1143 assertThat(edition.get(), is(24));
1147 public void subscriptionUpdatesMultipleTimes() throws InterruptedException, ExecutionException, IOException {
1148 Future<Optional<UskSubscription>> uskSubscription = fcpClient.subscribeUsk().uri(URI).execute();
1149 connectAndAssert(() -> matchesFcpMessage("SubscribeUSK", "URI=" + URI));
1150 replyWithSubscribed();
1151 assertThat(uskSubscription.get().get().getUri(), is(URI));
1152 AtomicInteger edition = new AtomicInteger();
1153 CountDownLatch updated = new CountDownLatch(2);
1154 uskSubscription.get().get().onUpdate(e -> {
1156 updated.countDown();
1158 uskSubscription.get().get().onUpdate(e -> updated.countDown());
1159 sendUpdateNotification(23);
1160 assertThat("updated in time", updated.await(5, TimeUnit.SECONDS), is(true));
1161 assertThat(edition.get(), is(23));
1165 public void subscriptionCanBeCancelled() throws InterruptedException, ExecutionException, IOException {
1166 Future<Optional<UskSubscription>> uskSubscription = fcpClient.subscribeUsk().uri(URI).execute();
1167 connectAndAssert(() -> matchesFcpMessage("SubscribeUSK", "URI=" + URI));
1168 replyWithSubscribed();
1169 assertThat(uskSubscription.get().get().getUri(), is(URI));
1170 AtomicBoolean updated = new AtomicBoolean();
1171 uskSubscription.get().get().onUpdate(e -> updated.set(true));
1172 uskSubscription.get().get().cancel();
1173 readMessage(() -> matchesFcpMessage("UnsubscribeUSK", "Identifier=" + identifier));
1174 sendUpdateNotification(23);
1175 assertThat(updated.get(), is(false));
1178 private void replyWithSubscribed() throws IOException {
1179 fcpServer.writeLine(
1181 "Identifier=" + identifier,
1188 private void sendUpdateNotification(int edition, String... additionalLines) throws IOException {
1189 fcpServer.writeLine(
1190 "SubscribedUSKUpdate",
1191 "Identifier=" + identifier,
1193 "Edition=" + edition
1195 fcpServer.writeLine(additionalLines);
1196 fcpServer.writeLine("EndMessage");
1201 public class ClientGet {
1204 public void works() throws InterruptedException, ExecutionException, IOException {
1205 Future<Optional<Data>> dataFuture = fcpClient.clientGet().uri("KSK@foo.txt").execute();
1206 connectAndAssert(() -> matchesFcpMessage("ClientGet", "URI=KSK@foo.txt", "ReturnType=direct"));
1207 replyWithAllData("not-test", "Hello World", "text/plain;charset=latin-9");
1208 replyWithAllData(identifier, "Hello", "text/plain;charset=utf-8");
1209 Optional<Data> data = dataFuture.get();
1214 public void getFailedIsRecognized() throws InterruptedException, ExecutionException, IOException {
1215 Future<Optional<Data>> dataFuture = fcpClient.clientGet().uri("KSK@foo.txt").execute();
1216 connectAndAssert(() -> matchesFcpMessage("ClientGet", "URI=KSK@foo.txt"));
1217 replyWithGetFailed("not-test");
1218 replyWithGetFailed(identifier);
1219 Optional<Data> data = dataFuture.get();
1220 assertThat(data.isPresent(), is(false));
1224 public void getFailedForDifferentIdentifierIsIgnored()
1225 throws InterruptedException, ExecutionException, IOException {
1226 Future<Optional<Data>> dataFuture = fcpClient.clientGet().uri("KSK@foo.txt").execute();
1227 connectAndAssert(() -> matchesFcpMessage("ClientGet", "URI=KSK@foo.txt"));
1228 replyWithGetFailed("not-test");
1229 replyWithAllData(identifier, "Hello", "text/plain;charset=utf-8");
1230 Optional<Data> data = dataFuture.get();
1234 @Test(expected = ExecutionException.class)
1235 public void connectionClosedIsRecognized() throws InterruptedException, ExecutionException, IOException {
1236 Future<Optional<Data>> dataFuture = fcpClient.clientGet().uri("KSK@foo.txt").execute();
1237 connectAndAssert(() -> matchesFcpMessage("ClientGet", "URI=KSK@foo.txt"));
1243 public void withIgnoreData() throws InterruptedException, ExecutionException, IOException {
1244 fcpClient.clientGet().ignoreDataStore().uri("KSK@foo.txt").execute();
1245 connectAndAssert(() -> matchesFcpMessage("ClientGet", "URI=KSK@foo.txt", "IgnoreDS=true"));
1249 public void withDataStoreOnly() throws InterruptedException, ExecutionException, IOException {
1250 fcpClient.clientGet().dataStoreOnly().uri("KSK@foo.txt").execute();
1251 connectAndAssert(() -> matchesFcpMessage("ClientGet", "URI=KSK@foo.txt", "DSonly=true"));
1255 public void clientGetWithMaxSizeSettingSendsCorrectCommands()
1256 throws InterruptedException, ExecutionException, IOException {
1257 fcpClient.clientGet().maxSize(1048576).uri("KSK@foo.txt").execute();
1258 connectAndAssert(() -> matchesFcpMessage("ClientGet", "URI=KSK@foo.txt", "MaxSize=1048576"));
1262 public void clientGetWithPrioritySettingSendsCorrectCommands()
1263 throws InterruptedException, ExecutionException, IOException {
1264 fcpClient.clientGet().priority(Priority.interactive).uri("KSK@foo.txt").execute();
1265 connectAndAssert(() -> matchesFcpMessage("ClientGet", "URI=KSK@foo.txt", "PriorityClass=1"));
1269 public void clientGetWithRealTimeSettingSendsCorrectCommands()
1270 throws InterruptedException, ExecutionException, IOException {
1271 fcpClient.clientGet().realTime().uri("KSK@foo.txt").execute();
1272 connectAndAssert(() -> matchesFcpMessage("ClientGet", "URI=KSK@foo.txt", "RealTimeFlag=true"));
1276 public void clientGetWithGlobalSettingSendsCorrectCommands()
1277 throws InterruptedException, ExecutionException, IOException {
1278 fcpClient.clientGet().global().uri("KSK@foo.txt").execute();
1279 connectAndAssert(() -> matchesFcpMessage("ClientGet", "URI=KSK@foo.txt", "Global=true"));
1282 private void replyWithGetFailed(String identifier) throws IOException {
1283 fcpServer.writeLine(
1285 "Identifier=" + identifier,
1291 private void replyWithAllData(String identifier, String text, String contentType) throws IOException {
1292 fcpServer.writeLine(
1294 "Identifier=" + identifier,
1295 "DataLength=" + (text.length() + 1),
1296 "StartupTime=1435610539000",
1297 "CompletionTime=1435610540000",
1298 "Metadata.ContentType=" + contentType,
1304 private void verifyData(Optional<Data> data) throws IOException {
1305 assertThat(data.get().getMimeType(), is("text/plain;charset=utf-8"));
1306 assertThat(data.get().size(), is(6L));
1307 assertThat(ByteStreams.toByteArray(data.get().getInputStream()),
1308 is("Hello\n".getBytes(StandardCharsets.UTF_8)));
1313 public class ClientPut {
1316 public void sendsCorrectCommand() throws IOException, ExecutionException, InterruptedException {
1317 fcpClient.clientPut()
1318 .from(new ByteArrayInputStream("Hello\n".getBytes()))
1323 readMessage("Hello", this::matchesDirectClientPut);
1327 public void succeedsOnCorrectIdentifier() throws InterruptedException, ExecutionException, IOException {
1328 Future<Optional<Key>> key = fcpClient.clientPut()
1329 .from(new ByteArrayInputStream("Hello\n".getBytes()))
1334 readMessage("Hello", this::matchesDirectClientPut);
1335 replyWithPutFailed("not-the-right-one");
1336 replyWithPutSuccessful(identifier);
1337 assertThat(key.get().get().getKey(), is("KSK@foo.txt"));
1341 public void failsOnCorrectIdentifier() throws InterruptedException, ExecutionException, IOException {
1342 Future<Optional<Key>> key = fcpClient.clientPut()
1343 .from(new ByteArrayInputStream("Hello\n".getBytes()))
1348 readMessage("Hello", this::matchesDirectClientPut);
1349 replyWithPutSuccessful("not-the-right-one");
1350 replyWithPutFailed(identifier);
1351 assertThat(key.get().isPresent(), is(false));
1355 public void renameIsSentCorrectly() throws InterruptedException, ExecutionException, IOException {
1356 fcpClient.clientPut()
1357 .named("otherName.txt")
1358 .from(new ByteArrayInputStream("Hello\n".getBytes()))
1363 readMessage("Hello", () -> allOf(
1364 hasHead("ClientPut"),
1365 hasParameters(1, 2, "TargetFilename=otherName.txt", "UploadFrom=direct", "DataLength=6",
1367 hasTail("EndMessage", "Hello")
1372 public void redirectIsSentCorrecly() throws IOException, ExecutionException, InterruptedException {
1373 fcpClient.clientPut().redirectTo("KSK@bar.txt").uri("KSK@foo.txt").execute();
1374 connectAndAssert(() ->
1375 matchesFcpMessage("ClientPut", "UploadFrom=redirect", "URI=KSK@foo.txt", "TargetURI=KSK@bar.txt"));
1379 public void withFileIsSentCorrectly() throws InterruptedException, ExecutionException, IOException {
1380 fcpClient.clientPut().from(new File("/tmp/data.txt")).uri("KSK@foo.txt").execute();
1381 connectAndAssert(() ->
1382 matchesFcpMessage("ClientPut", "UploadFrom=disk", "URI=KSK@foo.txt", "Filename=/tmp/data.txt"));
1387 private final File ddaFile;
1388 private final File fileToUpload;
1390 public DDA() throws IOException {
1391 ddaFile = createDdaFile();
1392 fileToUpload = new File(ddaFile.getParent(), "test.dat");
1395 private Matcher<List<String>> matchesFileClientPut(File file) {
1396 return matchesFcpMessage("ClientPut", "UploadFrom=disk", "URI=KSK@foo.txt", "Filename=" + file);
1400 public void completeDda() throws IOException, ExecutionException, InterruptedException {
1401 fcpClient.clientPut().from(fileToUpload).uri("KSK@foo.txt").execute();
1402 connectAndAssert(() -> matchesFileClientPut(fileToUpload));
1403 sendDdaRequired(identifier);
1404 readMessage(() -> matchesTestDDARequest(ddaFile));
1405 sendTestDDAReply(ddaFile.getParent(), ddaFile);
1406 readMessage(() -> matchesTestDDAResponse(ddaFile));
1407 writeTestDDAComplete(ddaFile);
1408 readMessage(() -> matchesFileClientPut(fileToUpload));
1412 public void ignoreOtherDda() throws IOException, ExecutionException, InterruptedException {
1413 fcpClient.clientPut().from(fileToUpload).uri("KSK@foo.txt").execute();
1414 connectAndAssert(() -> matchesFileClientPut(fileToUpload));
1415 sendDdaRequired(identifier);
1416 readMessage(() -> matchesTestDDARequest(ddaFile));
1417 sendTestDDAReply("/some-other-directory", ddaFile);
1418 sendTestDDAReply(ddaFile.getParent(), ddaFile);
1419 readMessage(() -> matchesTestDDAResponse(ddaFile));
1423 public void sendResponseIfFileUnreadable() throws IOException, ExecutionException, InterruptedException {
1424 fcpClient.clientPut().from(fileToUpload).uri("KSK@foo.txt").execute();
1425 connectAndAssert(() -> matchesFileClientPut(fileToUpload));
1426 sendDdaRequired(identifier);
1427 readMessage(() -> matchesTestDDARequest(ddaFile));
1428 sendTestDDAReply(ddaFile.getParent(), new File(ddaFile + ".foo"));
1429 readMessage(this::matchesFailedToReadResponse);
1433 public void clientPutDoesNotResendOriginalClientPutOnTestDDACompleteWithWrongDirectory()
1434 throws IOException, ExecutionException, InterruptedException {
1435 fcpClient.clientPut().from(fileToUpload).uri("KSK@foo.txt").execute();
1437 List<String> lines = fcpServer.collectUntil(is("EndMessage"));
1438 String identifier = extractIdentifier(lines);
1439 fcpServer.writeLine(
1441 "Directory=/some-other-directory",
1444 sendDdaRequired(identifier);
1445 lines = fcpServer.collectUntil(is("EndMessage"));
1446 assertThat(lines, matchesFcpMessage(
1448 "Directory=" + ddaFile.getParent(),
1449 "WantReadDirectory=true",
1450 "WantWriteDirectory=false"
1454 private Matcher<List<String>> matchesFailedToReadResponse() {
1455 return matchesFcpMessage(
1457 "Directory=" + ddaFile.getParent(),
1458 "ReadContent=failed-to-read"
1462 private void writeTestDDAComplete(File tempFile) throws IOException {
1463 fcpServer.writeLine(
1465 "Directory=" + tempFile.getParent(),
1466 "ReadDirectoryAllowed=true",
1471 private Matcher<List<String>> matchesTestDDAResponse(File tempFile) {
1472 return matchesFcpMessage(
1474 "Directory=" + tempFile.getParent(),
1475 "ReadContent=test-content"
1479 private void sendTestDDAReply(String directory, File tempFile) throws IOException {
1480 fcpServer.writeLine(
1482 "Directory=" + directory,
1483 "ReadFilename=" + tempFile,
1488 private Matcher<List<String>> matchesTestDDARequest(File tempFile) {
1489 return matchesFcpMessage(
1491 "Directory=" + tempFile.getParent(),
1492 "WantReadDirectory=true",
1493 "WantWriteDirectory=false"
1497 private void sendDdaRequired(String identifier) throws IOException {
1498 fcpServer.writeLine(
1500 "Identifier=" + identifier,
1508 private void replyWithPutSuccessful(String identifier) throws IOException {
1509 fcpServer.writeLine(
1512 "Identifier=" + identifier,
1517 private void replyWithPutFailed(String identifier) throws IOException {
1518 fcpServer.writeLine(
1520 "Identifier=" + identifier,
1525 private Matcher<List<String>> matchesDirectClientPut() {
1527 hasHead("ClientPut"),
1528 hasParameters(1, 2, "UploadFrom=direct", "DataLength=6", "URI=KSK@foo.txt"),
1529 hasTail("EndMessage", "Hello")
1533 private File createDdaFile() throws IOException {
1534 File tempFile = File.createTempFile("test-dda-", ".dat");
1535 tempFile.deleteOnExit();
1536 Files.write("test-content", tempFile, StandardCharsets.UTF_8);
1541 public void clientPutDoesNotReactToProtocolErrorForDifferentIdentifier()
1542 throws InterruptedException, ExecutionException, IOException {
1543 Future<Optional<Key>> key = fcpClient.clientPut().from(new File("/tmp/data.txt")).uri("KSK@foo.txt").execute();
1545 List<String> lines = fcpServer.collectUntil(is("EndMessage"));
1546 String identifier = extractIdentifier(lines);
1547 fcpServer.writeLine(
1549 "Identifier=not-the-right-one",
1553 fcpServer.writeLine(
1555 "Identifier=" + identifier,
1559 assertThat(key.get().get().getKey(), is("KSK@foo.txt"));
1563 public void clientPutAbortsOnProtocolErrorOtherThan25()
1564 throws InterruptedException, ExecutionException, IOException {
1565 Future<Optional<Key>> key = fcpClient.clientPut().from(new File("/tmp/data.txt")).uri("KSK@foo.txt").execute();
1567 List<String> lines = fcpServer.collectUntil(is("EndMessage"));
1568 String identifier = extractIdentifier(lines);
1569 fcpServer.writeLine(
1571 "Identifier=" + identifier,
1575 assertThat(key.get().isPresent(), is(false));
1579 public void clientPutSendsNotificationsForGeneratedKeys()
1580 throws InterruptedException, ExecutionException, IOException {
1581 List<String> generatedKeys = new CopyOnWriteArrayList<>();
1582 Future<Optional<Key>> key = fcpClient.clientPut()
1583 .onKeyGenerated(generatedKeys::add)
1584 .from(new ByteArrayInputStream("Hello\n".getBytes()))
1589 List<String> lines = fcpServer.collectUntil(is("Hello"));
1590 String identifier = extractIdentifier(lines);
1591 fcpServer.writeLine(
1593 "Identifier=" + identifier,
1597 replyWithPutSuccessful(identifier);
1598 assertThat(key.get().get().getKey(), is("KSK@foo.txt"));
1599 assertThat(generatedKeys, contains("KSK@foo.txt"));
1604 public class ConfigCommand {
1606 public class GetConfig {
1609 public void defaultFcpClientCanGetConfigWithoutDetails()
1610 throws InterruptedException, ExecutionException, IOException {
1611 Future<ConfigData> configData = fcpClient.getConfig().execute();
1612 connectAndAssert(() -> matchesFcpMessage("GetConfig", "Identifier=" + identifier));
1613 replyWithConfigData();
1614 assertThat(configData.get(), notNullValue());
1618 public void defaultFcpClientCanGetConfigWithCurrent()
1619 throws InterruptedException, ExecutionException, IOException {
1620 Future<ConfigData> configData = fcpClient.getConfig().withCurrent().execute();
1621 connectAndAssert(() -> matchesGetConfigWithAdditionalParameter("WithCurrent"));
1622 replyWithConfigData("current.foo=bar");
1623 assertThat(configData.get().getCurrent("foo"), is("bar"));
1627 public void defaultFcpClientCanGetConfigWithDefaults()
1628 throws InterruptedException, ExecutionException, IOException {
1629 Future<ConfigData> configData = fcpClient.getConfig().withDefaults().execute();
1630 connectAndAssert(() -> matchesGetConfigWithAdditionalParameter("WithDefaults"));
1631 replyWithConfigData("default.foo=bar");
1632 assertThat(configData.get().getDefault("foo"), is("bar"));
1636 public void defaultFcpClientCanGetConfigWithSortOrder()
1637 throws InterruptedException, ExecutionException, IOException {
1638 Future<ConfigData> configData = fcpClient.getConfig().withSortOrder().execute();
1639 connectAndAssert(() -> matchesGetConfigWithAdditionalParameter("WithSortOrder"));
1640 replyWithConfigData("sortOrder.foo=17");
1641 assertThat(configData.get().getSortOrder("foo"), is(17));
1645 public void defaultFcpClientCanGetConfigWithExpertFlag()
1646 throws InterruptedException, ExecutionException, IOException {
1647 Future<ConfigData> configData = fcpClient.getConfig().withExpertFlag().execute();
1648 connectAndAssert(() -> matchesGetConfigWithAdditionalParameter("WithExpertFlag"));
1649 replyWithConfigData("expertFlag.foo=true");
1650 assertThat(configData.get().getExpertFlag("foo"), is(true));
1654 public void defaultFcpClientCanGetConfigWithForceWriteFlag()
1655 throws InterruptedException, ExecutionException, IOException {
1656 Future<ConfigData> configData = fcpClient.getConfig().withForceWriteFlag().execute();
1657 connectAndAssert(() -> matchesGetConfigWithAdditionalParameter("WithForceWriteFlag"));
1658 replyWithConfigData("forceWriteFlag.foo=true");
1659 assertThat(configData.get().getForceWriteFlag("foo"), is(true));
1663 public void defaultFcpClientCanGetConfigWithShortDescription()
1664 throws InterruptedException, ExecutionException, IOException {
1665 Future<ConfigData> configData = fcpClient.getConfig().withShortDescription().execute();
1666 connectAndAssert(() -> matchesGetConfigWithAdditionalParameter("WithShortDescription"));
1667 replyWithConfigData("shortDescription.foo=bar");
1668 assertThat(configData.get().getShortDescription("foo"), is("bar"));
1672 public void defaultFcpClientCanGetConfigWithLongDescription()
1673 throws InterruptedException, ExecutionException, IOException {
1674 Future<ConfigData> configData = fcpClient.getConfig().withLongDescription().execute();
1675 connectAndAssert(() -> matchesGetConfigWithAdditionalParameter("WithLongDescription"));
1676 replyWithConfigData("longDescription.foo=bar");
1677 assertThat(configData.get().getLongDescription("foo"), is("bar"));
1681 public void defaultFcpClientCanGetConfigWithDataTypes()
1682 throws InterruptedException, ExecutionException, IOException {
1683 Future<ConfigData> configData = fcpClient.getConfig().withDataTypes().execute();
1684 connectAndAssert(() -> matchesGetConfigWithAdditionalParameter("WithDataTypes"));
1685 replyWithConfigData("dataType.foo=number");
1686 assertThat(configData.get().getDataType("foo"), is("number"));
1689 private Matcher<List<String>> matchesGetConfigWithAdditionalParameter(String additionalParameter) {
1690 return matchesFcpMessage(
1692 "Identifier=" + identifier,
1693 additionalParameter + "=true"
1699 public class ModifyConfig {
1702 public void defaultFcpClientCanModifyConfigData()
1703 throws InterruptedException, ExecutionException, IOException {
1704 Future<ConfigData> newConfigData = fcpClient.modifyConfig().set("foo.bar").to("baz").execute();
1705 connectAndAssert(() -> matchesFcpMessage(
1707 "Identifier=" + identifier,
1710 replyWithConfigData("current.foo.bar=baz");
1711 assertThat(newConfigData.get().getCurrent("foo.bar"), is("baz"));
1716 private void replyWithConfigData(String... additionalLines) throws IOException {
1717 fcpServer.writeLine("ConfigData", "Identifier=" + identifier);
1718 fcpServer.writeLine(additionalLines);
1719 fcpServer.writeLine("EndMessage");
1724 public class NodeInformation {
1727 public void defaultFcpClientCanGetNodeInformation() throws InterruptedException, ExecutionException, IOException {
1728 Future<NodeData> nodeData = fcpClient.getNode().execute();
1729 connectAndAssert(() -> matchesGetNode(false, false, false));
1730 replyWithNodeData();
1731 assertThat(nodeData.get(), notNullValue());
1732 assertThat(nodeData.get().getNodeRef().isOpennet(), is(false));
1736 public void defaultFcpClientCanGetNodeInformationWithOpennetRef()
1737 throws InterruptedException, ExecutionException, IOException {
1738 Future<NodeData> nodeData = fcpClient.getNode().opennetRef().execute();
1739 connectAndAssert(() -> matchesGetNode(true, false, false));
1740 replyWithNodeData("opennet=true");
1741 assertThat(nodeData.get().getVersion().toString(), is("Fred,0.7,1.0,1466"));
1742 assertThat(nodeData.get().getNodeRef().isOpennet(), is(true));
1746 public void defaultFcpClientCanGetNodeInformationWithPrivateData()
1747 throws InterruptedException, ExecutionException, IOException {
1748 Future<NodeData> nodeData = fcpClient.getNode().includePrivate().execute();
1749 connectAndAssert(() -> matchesGetNode(false, true, false));
1750 replyWithNodeData("ark.privURI=SSK@XdHMiRl");
1751 assertThat(nodeData.get().getARK().getPrivateURI(), is("SSK@XdHMiRl"));
1755 public void defaultFcpClientCanGetNodeInformationWithVolatileData()
1756 throws InterruptedException, ExecutionException, IOException {
1757 Future<NodeData> nodeData = fcpClient.getNode().includeVolatile().execute();
1758 connectAndAssert(() -> matchesGetNode(false, false, true));
1759 replyWithNodeData("volatile.freeJavaMemory=205706528");
1760 assertThat(nodeData.get().getVolatile("freeJavaMemory"), is("205706528"));
1763 private Matcher<List<String>> matchesGetNode(boolean withOpennetRef, boolean withPrivate, boolean withVolatile) {
1764 return matchesFcpMessage(
1766 "Identifier=" + identifier,
1767 "GiveOpennetRef=" + withOpennetRef,
1768 "WithPrivate=" + withPrivate,
1769 "WithVolatile=" + withVolatile
1773 private void replyWithNodeData(String... additionalLines) throws IOException {
1774 fcpServer.writeLine(
1776 "Identifier=" + identifier,
1777 "ark.pubURI=SSK@3YEf.../ark",
1780 "version=Fred,0.7,1.0,1466",
1781 "lastGoodVersion=Fred,0.7,1.0,1466"
1783 fcpServer.writeLine(additionalLines);
1784 fcpServer.writeLine("EndMessage");