Further reduce dependencies on a Sone for downloading.
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sat, 13 Sep 2014 17:00:57 +0000 (19:00 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sat, 13 Sep 2014 17:00:57 +0000 (19:00 +0200)
src/main/java/net/pterodactylus/sone/core/FreenetInterface.java
src/main/java/net/pterodactylus/sone/core/SoneDownloaderImpl.java
src/main/java/net/pterodactylus/sone/core/SoneUpdater.java [deleted file]
src/main/java/net/pterodactylus/sone/data/Sone.java
src/main/java/net/pterodactylus/sone/freenet/Key.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/core/FreenetInterfaceTest.java
src/test/java/net/pterodactylus/sone/core/SoneDownloaderTest.java
src/test/java/net/pterodactylus/sone/freenet/KeyTest.java [new file with mode: 0644]

index b389587..9d1a7ed 100644 (file)
 
 package net.pterodactylus.sone.core;
 
+import static freenet.keys.USK.create;
+import static java.lang.String.format;
+import static java.util.logging.Level.WARNING;
+import static net.pterodactylus.sone.freenet.Key.routingKey;
+
 import java.net.MalformedURLException;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.concurrent.TimeUnit;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -182,33 +186,30 @@ public class FreenetInterface {
                }
        }
 
-       public void registerUsk(final Sone sone, final SoneUpdater soneUpdater) {
+       public void registerActiveUsk(FreenetURI requestUri,
+                       USKCallback uskCallback) {
                try {
-                       logger.log(Level.FINE, String.format("Registering Sone “%s” for USK updates at %s…", sone, sone.getRequestUri().setMetaString(new String[] { "sone.xml" })));
-                       USKCallback uskCallback = new USKCallback() {
-
-                               @Override
-                               @SuppressWarnings("synthetic-access")
-                               public void onFoundEdition(long edition, USK key, ObjectContainer objectContainer, ClientContext clientContext, boolean metadata, short codec, byte[] data, boolean newKnownGood, boolean newSlotToo) {
-                                       logger.log(Level.FINE, String.format("Found USK update for Sone “%s” at %s, new known good: %s, new slot too: %s.", sone, key, newKnownGood, newSlotToo));
-                                       soneUpdater.updateSone(edition);
-                               }
-
-                               @Override
-                               public short getPollingPriorityProgress() {
-                                       return RequestStarter.INTERACTIVE_PRIORITY_CLASS;
-                               }
+                       soneUskCallbacks.put(routingKey(requestUri), uskCallback);
+                       node.clientCore.uskManager.subscribe(create(requestUri),
+                                       uskCallback, true, (RequestClient) client);
+               } catch (MalformedURLException mue1) {
+                       logger.log(WARNING, format("Could not subscribe USK “%s”!",
+                                       requestUri), mue1);
+               }
+       }
 
-                               @Override
-                               public short getPollingPriorityNormal() {
-                                       return RequestStarter.INTERACTIVE_PRIORITY_CLASS;
-                               }
-                       };
-                       soneUskCallbacks.put(sone.getId(), uskCallback);
-                       boolean runBackgroundFetch = (System.currentTimeMillis() - sone.getTime()) < TimeUnit.DAYS.toMillis(7);
-                       node.clientCore.uskManager.subscribe(USK.create(sone.getRequestUri()), uskCallback, runBackgroundFetch, (RequestClient) client);
+       public void registerPassiveUsk(FreenetURI requestUri,
+                       USKCallback uskCallback) {
+               try {
+                       soneUskCallbacks.put(routingKey(requestUri), uskCallback);
+                       node.clientCore
+                                       .uskManager
+                                       .subscribe(create(requestUri), uskCallback, false,
+                                                       (RequestClient) client);
                } catch (MalformedURLException mue1) {
-                       logger.log(Level.WARNING, String.format("Could not subscribe USK “%s”!", sone.getRequestUri()), mue1);
+                       logger.log(WARNING,
+                                       format("Could not subscribe USK “%s”!", requestUri),
+                                       mue1);
                }
        }
 
index d59f1f5..1e1484d 100644 (file)
 
 package net.pterodactylus.sone.core;
 
+import static java.lang.String.format;
+import static java.lang.System.currentTimeMillis;
+import static java.util.concurrent.TimeUnit.DAYS;
+
 import java.io.InputStream;
 import java.net.MalformedURLException;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -46,9 +51,15 @@ import net.pterodactylus.util.xml.SimpleXML;
 import net.pterodactylus.util.xml.XML;
 
 import freenet.client.FetchResult;
+import freenet.client.async.ClientContext;
+import freenet.client.async.USKCallback;
 import freenet.keys.FreenetURI;
+import freenet.keys.USK;
+import freenet.node.RequestStarter;
 import freenet.support.api.Bucket;
 
+import com.db4o.ObjectContainer;
+
 import com.google.common.annotations.VisibleForTesting;
 import org.w3c.dom.Document;
 
@@ -103,16 +114,46 @@ public class SoneDownloaderImpl extends AbstractService implements SoneDownloade
                if (!sones.add(sone)) {
                        freenetInterface.unregisterUsk(sone);
                }
-               freenetInterface.registerUsk(sone, new SoneUpdater() {
+               final USKCallback uskCallback = new USKCallback() {
+
                        @Override
-                       public void updateSone(long edition) {
+                       @SuppressWarnings("synthetic-access")
+                       public void onFoundEdition(long edition, USK key,
+                                       ObjectContainer objectContainer,
+                                       ClientContext clientContext, boolean metadata,
+                                       short codec, byte[] data, boolean newKnownGood,
+                                       boolean newSlotToo) {
+                               logger.log(Level.FINE, format(
+                                               "Found USK update for Sone “%s” at %s, new known good: %s, new slot too: %s.",
+                                               sone, key, newKnownGood, newSlotToo));
                                if (edition > sone.getLatestEdition()) {
                                        sone.setLatestEdition(edition);
                                        new Thread(fetchSoneAction(sone),
                                                        "Sone Downloader").start();
                                }
                        }
-               });
+
+                       @Override
+                       public short getPollingPriorityProgress() {
+                               return RequestStarter.INTERACTIVE_PRIORITY_CLASS;
+                       }
+
+                       @Override
+                       public short getPollingPriorityNormal() {
+                               return RequestStarter.INTERACTIVE_PRIORITY_CLASS;
+                       }
+               };
+               if (soneHasBeenActiveRecently(sone)) {
+                       freenetInterface.registerActiveUsk(sone.getRequestUri(),
+                                       uskCallback);
+               } else {
+                       freenetInterface.registerPassiveUsk(sone.getRequestUri(),
+                                       uskCallback);
+               }
+       }
+
+       private boolean soneHasBeenActiveRecently(Sone sone) {
+               return (currentTimeMillis() - sone.getTime()) < DAYS.toMillis(7);
        }
 
        private void fetchSone(Sone sone) {
diff --git a/src/main/java/net/pterodactylus/sone/core/SoneUpdater.java b/src/main/java/net/pterodactylus/sone/core/SoneUpdater.java
deleted file mode 100644 (file)
index 5cdefbe..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-package net.pterodactylus.sone.core;
-
-import net.pterodactylus.sone.data.Sone;
-
-/**
- * Component that decides whether a Sone needs to be downloaded because a
- * newer edition was found.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public interface SoneUpdater {
-
-       void updateSone(long edition);
-
-}
index 2263192..1b76588 100644 (file)
@@ -27,13 +27,17 @@ import java.util.Comparator;
 import java.util.List;
 import java.util.Set;
 
-import net.pterodactylus.sone.core.Options;
+import javax.annotation.Nonnegative;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
 import net.pterodactylus.sone.freenet.wot.Identity;
 import net.pterodactylus.sone.freenet.wot.OwnIdentity;
 import net.pterodactylus.sone.template.SoneAccessor;
 
 import freenet.keys.FreenetURI;
 
+import com.google.common.base.Function;
 import com.google.common.base.Predicate;
 import com.google.common.primitives.Ints;
 
@@ -167,6 +171,17 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
                }
        };
 
+       public static final Function<Sone, String> toSoneXmlUri =
+                       new Function<Sone, String>() {
+                               @Nonnull
+                               @Override
+                               public String apply(@Nullable Sone input) {
+                                       return input.getRequestUri()
+                                                       .setMetaString(new String[] { "sone.xml" })
+                                                       .toString();
+                               }
+                       };
+
        /**
         * Returns the identity of this Sone.
         *
diff --git a/src/main/java/net/pterodactylus/sone/freenet/Key.java b/src/main/java/net/pterodactylus/sone/freenet/Key.java
new file mode 100644 (file)
index 0000000..f21e2f6
--- /dev/null
@@ -0,0 +1,67 @@
+package net.pterodactylus.sone.freenet;
+
+import static freenet.support.Base64.encode;
+import static java.lang.String.format;
+
+import freenet.keys.FreenetURI;
+
+import com.google.common.annotations.VisibleForTesting;
+
+/**
+ * Encapsulates the parts of a {@link FreenetURI} that do not change while
+ * being converted from SSK to USK and/or back.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Key {
+
+       private final byte[] routingKey;
+       private final byte[] cryptoKey;
+       private final byte[] extra;
+
+       private Key(byte[] routingKey, byte[] cryptoKey, byte[] extra) {
+               this.routingKey = routingKey;
+               this.cryptoKey = cryptoKey;
+               this.extra = extra;
+       }
+
+       @VisibleForTesting
+       public String getRoutingKey() {
+               return encode(routingKey);
+       }
+
+       @VisibleForTesting
+       public String getCryptoKey() {
+               return encode(cryptoKey);
+       }
+
+       @VisibleForTesting
+       public String getExtra() {
+               return encode(extra);
+       }
+
+       public FreenetURI toUsk(String docName, long edition, String... paths) {
+               return new FreenetURI("USK", docName, paths, routingKey, cryptoKey,
+                               extra, edition);
+       }
+
+       public FreenetURI toSsk(String docName, String... paths) {
+               return new FreenetURI("SSK", docName, paths, routingKey, cryptoKey,
+                               extra);
+       }
+
+       public FreenetURI toSsk(String docName, long edition, String... paths) {
+               return new FreenetURI("SSK", format("%s-%d", docName, edition), paths,
+                               routingKey, cryptoKey, extra, edition);
+       }
+
+       public static Key from(FreenetURI freenetURI) {
+               return new Key(freenetURI.getRoutingKey(), freenetURI.getCryptoKey(),
+                               freenetURI.getExtra());
+       }
+
+       public static String routingKey(FreenetURI freenetURI) {
+               return from(freenetURI).getRoutingKey();
+       }
+
+}
index d2a1121..aa261f5 100644 (file)
@@ -5,9 +5,6 @@ import static freenet.client.InsertException.INTERNAL_ERROR;
 import static freenet.keys.InsertableClientSSK.createRandom;
 import static freenet.node.RequestStarter.INTERACTIVE_PRIORITY_CLASS;
 import static freenet.node.RequestStarter.PREFETCH_PRIORITY_CLASS;
-import static java.lang.System.currentTimeMillis;
-import static java.util.concurrent.TimeUnit.DAYS;
-import static java.util.concurrent.TimeUnit.SECONDS;
 import static net.pterodactylus.sone.Matchers.delivers;
 import static net.pterodactylus.sone.TestUtil.setFinalField;
 import static org.hamcrest.MatcherAssert.assertThat;
@@ -19,7 +16,6 @@ import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyBoolean;
 import static org.mockito.Matchers.anyShort;
 import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -31,7 +27,6 @@ import static org.mockito.Mockito.withSettings;
 import java.io.IOException;
 import java.net.MalformedURLException;
 import java.util.HashMap;
-import java.util.concurrent.CountDownLatch;
 
 import net.pterodactylus.sone.TestUtil;
 import net.pterodactylus.sone.core.FreenetInterface.Callback;
@@ -73,8 +68,6 @@ import com.google.common.eventbus.EventBus;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
 
 /**
  * Unit test for {@link FreenetInterface}.
@@ -92,8 +85,6 @@ public class FreenetInterfaceTest {
        private FreenetInterface freenetInterface;
        private final Sone sone = mock(Sone.class);
        private final ArgumentCaptor<USKCallback> callbackCaptor = forClass(USKCallback.class);
-       private final SoneDownloader soneDownloader = mock(SoneDownloader.class);
-       private final SoneUpdater soneUpdater = mock(SoneUpdater.class);
        private final Image image = mock(Image.class);
        private InsertToken insertToken;
 
@@ -211,25 +202,11 @@ public class FreenetInterfaceTest {
        @Test
        public void soneWithWrongRequestUriWillNotBeSubscribed() throws MalformedURLException {
                when(sone.getRequestUri()).thenReturn(new FreenetURI("KSK@GPLv3.txt"));
-               freenetInterface.registerUsk(sone, null);
+               freenetInterface.registerUsk(new FreenetURI("KSK@GPLv3.txt"), null);
                verify(uskManager, never()).subscribe(any(USK.class), any(USKCallback.class), anyBoolean(), any(RequestClient.class));
        }
 
        @Test
-       public void registeringAUskForARecentlyModifiedSone() throws MalformedURLException {
-               when(sone.getTime()).thenReturn(currentTimeMillis() - DAYS.toMillis(1));
-               freenetInterface.registerUsk(sone, null);
-               verify(uskManager).subscribe(any(USK.class), any(USKCallback.class), eq(true), eq((RequestClient) highLevelSimpleClient));
-       }
-
-       @Test
-       public void registeringAUskForAnOldSone() throws MalformedURLException {
-               when(sone.getTime()).thenReturn(currentTimeMillis() - DAYS.toMillis(365));
-               freenetInterface.registerUsk(sone, null);
-               verify(uskManager).subscribe(any(USK.class), any(USKCallback.class), eq(false), eq((RequestClient) highLevelSimpleClient));
-       }
-
-       @Test
        public void registeringAUsk() {
                FreenetURI freenetUri = createRandom(randomSource, "test-0").getURI().uskForSSK();
                Callback callback = mock(Callback.class);
@@ -268,39 +245,26 @@ public class FreenetInterfaceTest {
        }
 
        @Test
-       public void unregisteringARegisteredSoneUnregistersTheSone() {
-               freenetInterface.registerUsk(sone, null);
+       public void unregisteringARegisteredSoneUnregistersTheSone()
+       throws MalformedURLException {
+               freenetInterface.registerActiveUsk(sone.getRequestUri(), mock(USKCallback.class));
                freenetInterface.unregisterUsk(sone);
                verify(uskManager).unsubscribe(any(USK.class), any(USKCallback.class));
        }
 
        @Test
        public void unregisteringASoneWithAWrongRequestKeyWillNotUnsubscribe() throws MalformedURLException {
-               freenetInterface.registerUsk(sone, null);
                when(sone.getRequestUri()).thenReturn(new FreenetURI("KSK@GPLv3.txt"));
+               freenetInterface.registerUsk(sone.getRequestUri(), null);
                freenetInterface.unregisterUsk(sone);
                verify(uskManager, never()).unsubscribe(any(USK.class), any(USKCallback.class));
        }
 
        @Test
-       public void callbackPrioritiesAreInteractive() {
-               freenetInterface.registerUsk(sone, null);
-               assertThat(callbackCaptor.getValue().getPollingPriorityNormal(), is(INTERACTIVE_PRIORITY_CLASS));
-               assertThat(callbackCaptor.getValue().getPollingPriorityProgress(), is(INTERACTIVE_PRIORITY_CLASS));
-       }
-
-       @Test
-       public void callbackForRegisteredSoneWithHigherEditionTriggersDownload() throws InterruptedException {
-               freenetInterface.registerUsk(sone, soneUpdater);
-               callbackCaptor.getValue().onFoundEdition(1, null, null, null, false, (short) 0, null, false, false);
-               verify(soneUpdater).updateSone(1);
-       }
-
-       @Test
        public void callbackForNormalUskUsesDifferentPriorities() {
                Callback callback = mock(Callback.class);
-               FreenetURI uri = createRandom(randomSource, "test-0").getURI().uskForSSK();
-               freenetInterface.registerUsk(uri, callback);
+               FreenetURI soneUri = createRandom(randomSource, "test-0").getURI().uskForSSK();
+               freenetInterface.registerUsk(soneUri, callback);
                assertThat(callbackCaptor.getValue().getPollingPriorityNormal(), is(PREFETCH_PRIORITY_CLASS));
                assertThat(callbackCaptor.getValue().getPollingPriorityProgress(), is(INTERACTIVE_PRIORITY_CLASS));
        }
index 24a1ac9..0f1c2a2 100644 (file)
@@ -1,7 +1,9 @@
 package net.pterodactylus.sone.core;
 
 import static com.google.common.base.Optional.of;
+import static java.lang.System.currentTimeMillis;
 import static java.util.UUID.randomUUID;
+import static java.util.concurrent.TimeUnit.DAYS;
 import static net.pterodactylus.sone.data.Sone.SoneStatus.downloading;
 import static net.pterodactylus.sone.data.Sone.SoneStatus.idle;
 import static net.pterodactylus.sone.data.Sone.SoneStatus.unknown;
@@ -33,6 +35,7 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 import net.pterodactylus.sone.core.FreenetInterface.Fetched;
 import net.pterodactylus.sone.data.Album;
@@ -52,9 +55,15 @@ import net.pterodactylus.sone.freenet.wot.Identity;
 
 import freenet.client.ClientMetadata;
 import freenet.client.FetchResult;
+import freenet.client.async.ClientContext;
+import freenet.client.async.USKCallback;
 import freenet.keys.FreenetURI;
+import freenet.keys.USK;
+import freenet.node.RequestStarter;
 import freenet.support.api.Bucket;
 
+import com.db4o.ObjectContainer;
+
 import com.google.common.base.Optional;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableSet;
@@ -75,7 +84,6 @@ public class SoneDownloaderTest {
        private final Core core = mock(Core.class);
        private final FreenetInterface freenetInterface = mock(FreenetInterface.class);
        private final SoneDownloaderImpl soneDownloader = new SoneDownloaderImpl(core, freenetInterface);
-       private final SoneUpdater soneUpdater = mock(SoneUpdater.class);
        private final FreenetURI requestUri = mock(FreenetURI.class);
        private final Sone sone = mock(Sone.class);
        private final PostBuilder postBuilder = mock(PostBuilder.class);
@@ -97,6 +105,11 @@ public class SoneDownloaderTest {
                when(sone.getId()).thenReturn("identity");
                when(sone.getIdentity()).thenReturn(identity);
                when(sone.getRequestUri()).thenReturn(requestUri);
+               when(sone.getTime()).thenReturn(currentTimeMillis() - DAYS.toMillis(1));
+       }
+
+       private void setupSoneAsUnknown() {
+               when(sone.getTime()).thenReturn(0L);
        }
 
        @Before
@@ -342,7 +355,8 @@ public class SoneDownloaderTest {
        @Test
        public void addingASoneWillRegisterItsKey() {
                soneDownloader.addSone(sone);
-               verify(freenetInterface).registerUsk(eq(sone), any(SoneUpdater.class));
+               verify(freenetInterface).registerActiveUsk(eq(sone.getRequestUri()), any(
+                               USKCallback.class));
                verify(freenetInterface, never()).unregisterUsk(sone);
        }
 
@@ -350,7 +364,8 @@ public class SoneDownloaderTest {
        public void addingASoneTwiceWillAlsoDeregisterItsKey() {
                soneDownloader.addSone(sone);
                soneDownloader.addSone(sone);
-               verify(freenetInterface, times(2)).registerUsk(eq(sone), any(SoneUpdater.class));
+               verify(freenetInterface, times(2)).registerActiveUsk(eq(
+                               sone.getRequestUri()), any(USKCallback.class));
                verify(freenetInterface).unregisterUsk(sone);
        }
 
@@ -731,6 +746,7 @@ public class SoneDownloaderTest {
 
        @Test
        public void notBeingAbleToFetchAnUnknownSoneDoesNotUpdateCore() {
+               setupSoneAsUnknown();
                soneDownloader.fetchSoneAction(sone).run();
                verify(freenetInterface).fetchUri(requestUri);
                verifyThatSoneStatusWasChangedToDownloadingAndBackTo(unknown);
@@ -746,7 +762,6 @@ public class SoneDownloaderTest {
 
        @Test
        public void notBeingAbleToFetchAKnownSoneDoesNotUpdateCore() {
-               when(sone.getTime()).thenReturn(1000L);
                soneDownloader.fetchSoneAction(sone).run();
                verify(freenetInterface).fetchUri(requestUri);
                verifyThatSoneStatusWasChangedToDownloadingAndBackTo(idle);
@@ -755,6 +770,7 @@ public class SoneDownloaderTest {
 
        @Test(expected = NullPointerException.class)
        public void exceptionWhileFetchingAnUnknownSoneDoesNotUpdateCore() {
+               setupSoneAsUnknown();
                when(freenetInterface.fetchUri(requestUri)).thenThrow(NullPointerException.class);
                try {
                        soneDownloader.fetchSoneAction(sone).run();
@@ -767,7 +783,6 @@ public class SoneDownloaderTest {
 
        @Test(expected = NullPointerException.class)
        public void exceptionWhileFetchingAKnownSoneDoesNotUpdateCore() {
-               when(sone.getTime()).thenReturn(1000L);
                when(freenetInterface.fetchUri(requestUri)).thenThrow(NullPointerException.class);
                try {
                        soneDownloader.fetchSoneAction(sone).run();
@@ -833,7 +848,7 @@ public class SoneDownloaderTest {
                when(freenetInterface.fetchUri(requestUri)).thenReturn(fetchResult);
                soneDownloader.fetchSone(sone, sone.getRequestUri(), true);
                verify(core, never()).updateSone(any(Sone.class));
-               verifyThatSoneStatusWasChangedToDownloadingAndBackTo(unknown);
+               verifyThatSoneStatusWasChangedToDownloadingAndBackTo(idle);
        }
 
        private Fetched createFetchResult(FreenetURI uri, InputStream inputStream) throws IOException {
diff --git a/src/test/java/net/pterodactylus/sone/freenet/KeyTest.java b/src/test/java/net/pterodactylus/sone/freenet/KeyTest.java
new file mode 100644 (file)
index 0000000..8fff7bd
--- /dev/null
@@ -0,0 +1,69 @@
+package net.pterodactylus.sone.freenet;
+
+import static freenet.support.Base64.encode;
+import static net.pterodactylus.sone.freenet.Key.from;
+import static net.pterodactylus.sone.freenet.Key.routingKey;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+import java.net.MalformedURLException;
+
+import freenet.keys.FreenetURI;
+
+import org.junit.Test;
+
+/**
+ * Unit test for {@link Key}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class KeyTest {
+
+       private final FreenetURI uri;
+       private final Key key;
+
+       public KeyTest() throws MalformedURLException {
+               uri = new FreenetURI(
+                               "SSK@NfUYvxDwU9vqb2mh-qdT~DYJ6U0XNbxMGGoLe0aCHJs,Miglsgix0VR56ZiPl4NgjnUd~UdrnHqIvXJ3KKHmxmI,AQACAAE/some-site-12/foo/bar.html");
+               key = from(uri);
+       }
+
+       @Test
+       public void keyCanBeCreatedFromFreenetUri() throws MalformedURLException {
+               assertThat(key.getRoutingKey(),
+                               is("NfUYvxDwU9vqb2mh-qdT~DYJ6U0XNbxMGGoLe0aCHJs"));
+               assertThat(key.getCryptoKey(),
+                               is("Miglsgix0VR56ZiPl4NgjnUd~UdrnHqIvXJ3KKHmxmI"));
+               assertThat(key.getExtra(), is("AQACAAE"));
+       }
+
+       @Test
+       public void keyCanBeConvertedToUsk() throws MalformedURLException {
+               FreenetURI uskUri = key.toUsk("other-site", 15, "some", "path.html");
+               assertThat(uskUri.toString(),
+                               is("USK@NfUYvxDwU9vqb2mh-qdT~DYJ6U0XNbxMGGoLe0aCHJs,Miglsgix0VR56ZiPl4NgjnUd~UdrnHqIvXJ3KKHmxmI,AQACAAE/other-site/15/some/path.html"));
+       }
+
+       @Test
+       public void keyCanBeConvertedToSskWithoutEdition()
+       throws MalformedURLException {
+               FreenetURI uskUri = key.toSsk("other-site", "some", "path.html");
+               assertThat(uskUri.toString(),
+                               is("SSK@NfUYvxDwU9vqb2mh-qdT~DYJ6U0XNbxMGGoLe0aCHJs,Miglsgix0VR56ZiPl4NgjnUd~UdrnHqIvXJ3KKHmxmI,AQACAAE/other-site/some/path.html"));
+       }
+
+       @Test
+       public void keyCanBeConvertedToSskWithEdition()
+       throws MalformedURLException {
+               FreenetURI uskUri = key.toSsk("other-site", 15, "some", "path.html");
+               assertThat(uskUri.toString(),
+                               is("SSK@NfUYvxDwU9vqb2mh-qdT~DYJ6U0XNbxMGGoLe0aCHJs,Miglsgix0VR56ZiPl4NgjnUd~UdrnHqIvXJ3KKHmxmI,AQACAAE/other-site-15/some/path.html"));
+       }
+
+       @Test
+       public void routingKeyIsExtractCorrectly() {
+               assertThat(routingKey(uri),
+                               is("NfUYvxDwU9vqb2mh-qdT~DYJ6U0XNbxMGGoLe0aCHJs"));
+       }
+
+}