<scope>test</scope>
</dependency>
<dependency>
+ <groupId>org.hamcrest</groupId>
+ <artifactId>hamcrest-all</artifactId>
+ <version>1.3</version>
+ </dependency>
+ <dependency>
<groupId>org.freenetproject</groupId>
<artifactId>fred</artifactId>
- <version>0.7.5.1405</version>
+ <version>0.7.5.1467.99.3</version>
<scope>provided</scope>
</dependency>
<dependency>
import net.pterodactylus.sone.data.Image;
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.data.TemporaryImage;
- import net.pterodactylus.util.logging.Logging;
-import com.db4o.ObjectContainer;
-
+ import com.google.common.base.Function;
import com.google.common.eventbus.EventBus;
import com.google.inject.Inject;
+ import com.google.inject.Singleton;
import freenet.client.ClientMetadata;
import freenet.client.FetchException;
+import freenet.client.FetchException.FetchExceptionMode;
import freenet.client.FetchResult;
import freenet.client.HighLevelSimpleClient;
- import freenet.client.HighLevelSimpleClientImpl;
import freenet.client.InsertBlock;
import freenet.client.InsertContext;
import freenet.client.InsertException;
FreenetURI currentUri = new FreenetURI(uri);
while (true) {
try {
- fetchResult = client.fetch(currentUri);
+ FetchResult fetchResult = client.fetch(currentUri);
return new Fetched(currentUri, fetchResult);
} catch (FetchException fe1) {
- if (fe1.getMode() == FetchException.PERMANENT_REDIRECT) {
+ if (fe1.getMode() == FetchExceptionMode.PERMANENT_REDIRECT) {
currentUri = fe1.newURI;
continue;
}
*/
@SuppressWarnings("synthetic-access")
public void cancel() {
- clientPutter.cancel(null, node.clientCore.clientContext);
+ clientPutter.cancel(node.clientCore.clientContext);
eventBus.post(new ImageInsertAbortedEvent(image));
++ bucket.free();
}
//
--- /dev/null
- ObjectContainer objectContainer,
+ /*
+ * Sone - SoneDownloader.java - Copyright © 2010–2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+ package net.pterodactylus.sone.core;
+
+ import static freenet.support.io.Closer.close;
+ import static java.lang.String.format;
+ import static java.lang.System.currentTimeMillis;
+ import static java.util.concurrent.TimeUnit.DAYS;
+ import static java.util.logging.Logger.getLogger;
+
+ import java.io.InputStream;
+ import java.util.HashSet;
+ import java.util.Set;
+ import java.util.logging.Level;
+ import java.util.logging.Logger;
+
+ import net.pterodactylus.sone.core.FreenetInterface.Fetched;
+ import net.pterodactylus.sone.data.Sone;
+ import net.pterodactylus.sone.data.Sone.SoneStatus;
+ import net.pterodactylus.util.service.AbstractService;
+
+ 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 freenet.support.io.Closer;
+ import com.db4o.ObjectContainer;
+
+ import com.google.common.annotations.VisibleForTesting;
+
+ /**
+ * The Sone downloader is responsible for download Sones as they are updated.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ public class SoneDownloaderImpl extends AbstractService implements SoneDownloader {
+
+ /** The logger. */
+ private static final Logger logger = getLogger("Sone.Downloader");
+
+ /** The maximum protocol version. */
+ private static final int MAX_PROTOCOL_VERSION = 0;
+
+ /** The core. */
+ private final Core core;
+ private final SoneParser soneParser;
+
+ /** The Freenet interface. */
+ private final FreenetInterface freenetInterface;
+
+ /** The sones to update. */
+ private final Set<Sone> sones = new HashSet<Sone>();
+
+ /**
+ * Creates a new Sone downloader.
+ *
+ * @param core
+ * The core
+ * @param freenetInterface
+ * The Freenet interface
+ */
+ public SoneDownloaderImpl(Core core, FreenetInterface freenetInterface) {
+ this(core, freenetInterface, new SoneParser(core));
+ }
+
+ /**
+ * Creates a new Sone downloader.
+ *
+ * @param core
+ * The core
+ * @param freenetInterface
+ * The Freenet interface
+ * @param soneParser
+ */
+ @VisibleForTesting
+ SoneDownloaderImpl(Core core, FreenetInterface freenetInterface, SoneParser soneParser) {
+ super("Sone Downloader", false);
+ this.core = core;
+ this.freenetInterface = freenetInterface;
+ this.soneParser = soneParser;
+ }
+
+ //
+ // ACTIONS
+ //
+
+ /**
+ * Adds the given Sone to the set of Sones that will be watched for updates.
+ *
+ * @param sone
+ * The Sone to add
+ */
+ @Override
+ public void addSone(final Sone sone) {
+ if (!sones.add(sone)) {
+ freenetInterface.unregisterUsk(sone);
+ }
+ final USKCallback uskCallback = new USKCallback() {
+
+ @Override
+ @SuppressWarnings("synthetic-access")
+ public void onFoundEdition(long edition, USK key,
+ 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) {
+ fetchSone(sone, sone.getRequestUri().sskForUSK());
+ }
+
+ /**
+ * Fetches the updated Sone. This method can be used to fetch a Sone from a
+ * specific URI.
+ *
+ * @param sone
+ * The Sone to fetch
+ * @param soneUri
+ * The URI to fetch the Sone from
+ */
+ @Override
+ public void fetchSone(Sone sone, FreenetURI soneUri) {
+ fetchSone(sone, soneUri, false);
+ }
+
+ /**
+ * Fetches the Sone from the given URI.
+ *
+ * @param sone
+ * The Sone to fetch
+ * @param soneUri
+ * The URI of the Sone to fetch
+ * @param fetchOnly
+ * {@code true} to only fetch and parse the Sone, {@code false}
+ * to {@link Core#updateSone(Sone) update} it in the core
+ * @return The downloaded Sone, or {@code null} if the Sone could not be
+ * downloaded
+ */
+ @Override
+ public Sone fetchSone(Sone sone, FreenetURI soneUri, boolean fetchOnly) {
+ logger.log(Level.FINE, String.format("Starting fetch for Sone “%s” from %s…", sone, soneUri));
+ FreenetURI requestUri = soneUri.setMetaString(new String[] { "sone.xml" });
+ sone.setStatus(SoneStatus.downloading);
+ try {
+ Fetched fetchResults = freenetInterface.fetchUri(requestUri);
+ if (fetchResults == null) {
+ /* TODO - mark Sone as bad. */
+ return null;
+ }
+ logger.log(Level.FINEST, String.format("Got %d bytes back.", fetchResults.getFetchResult().size()));
+ Sone parsedSone = parseSone(sone, fetchResults.getFetchResult(), fetchResults.getFreenetUri());
+ if (parsedSone != null) {
+ if (!fetchOnly) {
+ parsedSone.setStatus((parsedSone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle);
+ core.updateSone(parsedSone);
+ addSone(parsedSone);
+ }
+ }
+ return parsedSone;
+ } finally {
+ sone.setStatus((sone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle);
+ }
+ }
+
+ /**
+ * Parses a Sone from a fetch result.
+ *
+ * @param originalSone
+ * The sone to parse, or {@code null} if the Sone is yet unknown
+ * @param fetchResult
+ * The fetch result
+ * @param requestUri
+ * The requested URI
+ * @return The parsed Sone, or {@code null} if the Sone could not be parsed
+ */
+ private Sone parseSone(Sone originalSone, FetchResult fetchResult, FreenetURI requestUri) {
+ logger.log(Level.FINEST, String.format("Parsing FetchResult (%d bytes, %s) for %s…", fetchResult.size(), fetchResult.getMimeType(), originalSone));
+ Bucket soneBucket = fetchResult.asBucket();
+ InputStream soneInputStream = null;
+ try {
+ soneInputStream = soneBucket.getInputStream();
+ Sone parsedSone = soneParser.parseSone(originalSone,
+ soneInputStream);
+ if (parsedSone != null) {
+ parsedSone.setLatestEdition(requestUri.getEdition());
+ }
+ return parsedSone;
+ } catch (Exception e1) {
+ logger.log(Level.WARNING, String.format("Could not parse Sone from %s!", requestUri), e1);
+ } finally {
+ close(soneInputStream);
+ close(soneBucket);
+ }
+ return null;
+ }
+
+ @Override
+ public Runnable fetchSoneWithUriAction(final Sone sone) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ fetchSone(sone, sone.getRequestUri());
+ }
+ };
+ }
+
+ @Override
+ public Runnable fetchSoneAction(final Sone sone) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ fetchSone(sone);
+ }
+ };
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void serviceStop() {
+ for (Sone sone : sones) {
+ freenetInterface.unregisterUsk(sone);
+ }
+ }
+
+ }
package net.pterodactylus.sone.core;
- import static com.google.common.base.Preconditions.checkArgument;
+ import static java.lang.String.format;
+ import static java.lang.System.currentTimeMillis;
+ import static java.util.logging.Logger.getLogger;
import static net.pterodactylus.sone.data.Album.NOT_EMPTY;
++import java.io.Closeable;
+ import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
+import java.util.Set;
+ import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.pterodactylus.sone.data.Reply;
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.data.Sone.SoneStatus;
-import net.pterodactylus.sone.freenet.StringBucket;
import net.pterodactylus.sone.main.SonePlugin;
import net.pterodactylus.util.io.Closer;
- import net.pterodactylus.util.logging.Logging;
import net.pterodactylus.util.service.AbstractService;
import net.pterodactylus.util.template.HtmlFilter;
import net.pterodactylus.util.template.ReflectionAccessor;
import net.pterodactylus.util.template.TemplateParser;
import net.pterodactylus.util.template.XmlFilter;
+ import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Charsets;
+ import com.google.common.base.Optional;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.Ordering;
import com.google.common.eventbus.EventBus;
+ import com.google.common.eventbus.Subscribe;
-import freenet.client.async.ManifestElement;
import freenet.keys.FreenetURI;
+import freenet.support.api.Bucket;
+import freenet.support.api.ManifestElement;
+import freenet.support.api.RandomAccessBucket;
+import freenet.support.io.ArrayBucket;
/**
* A Sone inserter is responsible for inserting a Sone if it has changed.
*
* @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
- private class InsertInformation {
+ @VisibleForTesting
- class InsertInformation {
++ class InsertInformation implements Closeable {
- private final Set<Bucket> buckets = new HashSet<Bucket>();
+ /** All properties of the Sone, copied for thread safety. */
+ private final Map<String, Object> soneProperties = new HashMap<String, Object>();
+ private final String fingerprint;
+ private final ManifestCreator manifestCreator;
/**
* Creates a new insert information container.
return manifestEntries;
}
- //
- // PRIVATE METHODS
- //
++ @Override
++ public void close() {
++ manifestCreator.close();
++ }
+
- /**
- * Creates a new manifest element.
- *
- * @param name
- * The name of the file
- * @param contentType
- * The content type of the file
- * @param templateName
- * The name of the template to render
- * @return The manifest element
- */
- @SuppressWarnings("synthetic-access")
- private ManifestElement createManifestElement(String name, String contentType, String templateName) {
+ }
+
+ /**
+ * Creates manifest elements for an insert by rendering a template.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ @VisibleForTesting
- static class ManifestCreator {
++ static class ManifestCreator implements Closeable {
+
+ private final Core core;
+ private final Map<String, Object> soneProperties;
++ private final Set<Bucket> buckets = new HashSet<Bucket>();
+
+ ManifestCreator(Core core, Map<String, Object> soneProperties) {
+ this.core = core;
+ this.soneProperties = soneProperties;
+ }
+
+ public ManifestElement createManifestElement(String name, String contentType, String templateName) {
InputStreamReader templateInputStreamReader = null;
+ InputStream templateInputStream = null;
Template template;
try {
- templateInputStreamReader = new InputStreamReader(getClass().getResourceAsStream(templateName), utf8Charset);
+ templateInputStream = getClass().getResourceAsStream(templateName);
+ templateInputStreamReader = new InputStreamReader(templateInputStream, utf8Charset);
template = TemplateParser.parse(templateInputStreamReader);
} catch (TemplateException te1) {
logger.log(Level.SEVERE, String.format("Could not parse template “%s”!", templateName), te1);
import net.pterodactylus.util.config.Configuration;
import net.pterodactylus.util.config.ConfigurationException;
import net.pterodactylus.util.config.ExtendedConfigurationBackend;
- import net.pterodactylus.util.logging.Logging;
-import freenet.client.async.DatabaseDisabledException;
+import freenet.client.async.PersistenceDisabledException;
import freenet.pluginmanager.PluginRespirator;
import freenet.pluginmanager.PluginStore;
--- /dev/null
-import static freenet.client.InsertException.CANCELLED;
-import static freenet.client.InsertException.INTERNAL_ERROR;
+ package net.pterodactylus.sone.core;
+
-import net.pterodactylus.sone.freenet.StringBucket;
+ import static freenet.keys.InsertableClientSSK.createRandom;
+ import static freenet.node.RequestStarter.INTERACTIVE_PRIORITY_CLASS;
+ import static freenet.node.RequestStarter.PREFETCH_PRIORITY_CLASS;
+ import static net.pterodactylus.sone.Matchers.delivers;
+ import static net.pterodactylus.sone.TestUtil.setFinalField;
+ import static org.hamcrest.MatcherAssert.assertThat;
+ import static org.hamcrest.Matchers.is;
+ import static org.hamcrest.Matchers.notNullValue;
+ import static org.hamcrest.Matchers.nullValue;
+ import static org.mockito.ArgumentCaptor.forClass;
+ 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.doNothing;
+ import static org.mockito.Mockito.mock;
+ import static org.mockito.Mockito.never;
+ import static org.mockito.Mockito.times;
+ import static org.mockito.Mockito.verify;
+ import static org.mockito.Mockito.when;
+ import static org.mockito.Mockito.withSettings;
+
+ import java.io.IOException;
+ import java.net.MalformedURLException;
+ import java.util.HashMap;
+
+ import net.pterodactylus.sone.TestUtil;
+ import net.pterodactylus.sone.core.FreenetInterface.Callback;
+ import net.pterodactylus.sone.core.FreenetInterface.Fetched;
+ import net.pterodactylus.sone.core.FreenetInterface.InsertToken;
+ import net.pterodactylus.sone.core.FreenetInterface.InsertTokenSupplier;
+ import net.pterodactylus.sone.core.event.ImageInsertAbortedEvent;
+ import net.pterodactylus.sone.core.event.ImageInsertFailedEvent;
+ import net.pterodactylus.sone.core.event.ImageInsertFinishedEvent;
+ import net.pterodactylus.sone.core.event.ImageInsertStartedEvent;
+ import net.pterodactylus.sone.data.Image;
+ import net.pterodactylus.sone.data.impl.ImageImpl;
+ import net.pterodactylus.sone.data.Sone;
+ import net.pterodactylus.sone.data.TemporaryImage;
- FetchException fetchException = new FetchException(FetchException.PERMANENT_REDIRECT, newFreenetUri);
+
+ import freenet.client.ClientMetadata;
+ import freenet.client.FetchException;
++import freenet.client.FetchException.FetchExceptionMode;
+ import freenet.client.FetchResult;
+ import freenet.client.HighLevelSimpleClient;
+ import freenet.client.InsertBlock;
+ import freenet.client.InsertContext;
+ import freenet.client.InsertException;
++import freenet.client.InsertException.InsertExceptionMode;
+ import freenet.client.async.ClientPutter;
+ import freenet.client.async.USKCallback;
+ import freenet.client.async.USKManager;
+ import freenet.crypt.DummyRandomSource;
+ import freenet.crypt.RandomSource;
+ import freenet.keys.FreenetURI;
+ import freenet.keys.InsertableClientSSK;
+ import freenet.keys.USK;
+ import freenet.node.Node;
+ import freenet.node.NodeClientCore;
+ import freenet.node.RequestClient;
+ import freenet.support.Base64;
+ import freenet.support.api.Bucket;
++import freenet.support.io.ArrayBucket;
++import freenet.support.io.ResumeFailedException;
+
+ import com.google.common.eventbus.EventBus;
+ import org.junit.Before;
+ import org.junit.Test;
+ import org.mockito.ArgumentCaptor;
+
+ /**
+ * Unit test for {@link FreenetInterface}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ public class FreenetInterfaceTest {
+
+ private final EventBus eventBus = mock(EventBus.class);
+ private final Node node = mock(Node.class);
+ private final NodeClientCore nodeClientCore = mock(NodeClientCore.class);
+ private final HighLevelSimpleClient highLevelSimpleClient = mock(HighLevelSimpleClient.class, withSettings().extraInterfaces(RequestClient.class));
+ private final RandomSource randomSource = new DummyRandomSource();
+ private final USKManager uskManager = mock(USKManager.class);
+ private FreenetInterface freenetInterface;
+ private final Sone sone = mock(Sone.class);
+ private final ArgumentCaptor<USKCallback> callbackCaptor = forClass(USKCallback.class);
+ private final Image image = mock(Image.class);
+ private InsertToken insertToken;
++ private final Bucket bucket = mock(Bucket.class);
+
+ @Before
+ public void setupFreenetInterface() {
+ when(nodeClientCore.makeClient(anyShort(), anyBoolean(), anyBoolean())).thenReturn(highLevelSimpleClient);
+ setFinalField(node, "clientCore", nodeClientCore);
+ setFinalField(node, "random", randomSource);
+ setFinalField(nodeClientCore, "uskManager", uskManager);
+ freenetInterface = new FreenetInterface(eventBus, node);
+ insertToken = freenetInterface.new InsertToken(image);
++ insertToken.setBucket(bucket);
+ }
+
+ @Before
+ public void setupSone() {
+ InsertableClientSSK insertSsk = createRandom(randomSource, "test-0");
+ when(sone.getId()).thenReturn(Base64.encode(insertSsk.getURI().getRoutingKey()));
+ when(sone.getRequestUri()).thenReturn(insertSsk.getURI().uskForSSK());
+ }
+
+ @Before
+ public void setupCallbackCaptorAndUskManager() {
+ doNothing().when(uskManager).subscribe(any(USK.class), callbackCaptor.capture(), anyBoolean(), any(RequestClient.class));
+ }
+
+ @Test
+ public void canFetchUri() throws MalformedURLException, FetchException {
+ FreenetURI freenetUri = new FreenetURI("KSK@GPLv3.txt");
+ FetchResult fetchResult = createFetchResult();
+ when(highLevelSimpleClient.fetch(freenetUri)).thenReturn(fetchResult);
+ Fetched fetched = freenetInterface.fetchUri(freenetUri);
+ assertThat(fetched, notNullValue());
+ assertThat(fetched.getFetchResult(), is(fetchResult));
+ assertThat(fetched.getFreenetUri(), is(freenetUri));
+ }
+
+ @Test
+ public void fetchFollowsRedirect() throws MalformedURLException, FetchException {
+ FreenetURI freenetUri = new FreenetURI("KSK@GPLv2.txt");
+ FreenetURI newFreenetUri = new FreenetURI("KSK@GPLv3.txt");
+ FetchResult fetchResult = createFetchResult();
- FetchException fetchException = new FetchException(FetchException.ALL_DATA_NOT_FOUND);
++ FetchException fetchException = new FetchException(FetchExceptionMode.PERMANENT_REDIRECT, newFreenetUri);
+ when(highLevelSimpleClient.fetch(freenetUri)).thenThrow(fetchException);
+ when(highLevelSimpleClient.fetch(newFreenetUri)).thenReturn(fetchResult);
+ Fetched fetched = freenetInterface.fetchUri(freenetUri);
+ assertThat(fetched.getFetchResult(), is(fetchResult));
+ assertThat(fetched.getFreenetUri(), is(newFreenetUri));
+ }
+
+ @Test
+ public void fetchReturnsNullOnFetchExceptions() throws MalformedURLException, FetchException {
+ FreenetURI freenetUri = new FreenetURI("KSK@GPLv2.txt");
- Bucket bucket = new StringBucket("Some Data.");
++ FetchException fetchException = new FetchException(FetchExceptionMode.ALL_DATA_NOT_FOUND);
+ when(highLevelSimpleClient.fetch(freenetUri)).thenThrow(fetchException);
+ Fetched fetched = freenetInterface.fetchUri(freenetUri);
+ assertThat(fetched, nullValue());
+ }
+
+ private FetchResult createFetchResult() {
+ ClientMetadata clientMetadata = new ClientMetadata("text/plain");
- when(highLevelSimpleClient.insert(insertBlockCaptor.capture(), eq(false), eq((String) null), eq(false), eq(insertContext), eq(insertToken), anyShort())).thenReturn(clientPutter);
++ Bucket bucket = new ArrayBucket("Some Data.".getBytes());
+ return new FetchResult(clientMetadata, bucket);
+ }
+
+ @Test
+ public void insertingAnImage() throws SoneException, InsertException, IOException {
+ TemporaryImage temporaryImage = new TemporaryImage("image-id");
+ temporaryImage.setMimeType("image/png");
+ byte[] imageData = new byte[] { 1, 2, 3, 4 };
+ temporaryImage.setImageData(imageData);
+ Image image = new ImageImpl("image-id");
+ InsertToken insertToken = freenetInterface.new InsertToken(image);
+ InsertContext insertContext = mock(InsertContext.class);
+ when(highLevelSimpleClient.getInsertContext(anyBoolean())).thenReturn(insertContext);
+ ClientPutter clientPutter = mock(ClientPutter.class);
+ ArgumentCaptor<InsertBlock> insertBlockCaptor = forClass(InsertBlock.class);
- when(highLevelSimpleClient.insert(insertBlockCaptor.capture(), eq(false), eq((String) null), eq(false), eq(insertContext), eq(insertToken), anyShort())).thenThrow(InsertException.class);
++ when(highLevelSimpleClient.insert(insertBlockCaptor.capture(), eq((String) null), eq(false), eq(insertContext), eq(insertToken), anyShort())).thenReturn(clientPutter);
+ freenetInterface.insertImage(temporaryImage, image, insertToken);
+ assertThat(insertBlockCaptor.getValue().getData().getInputStream(), delivers(new byte[] { 1, 2, 3, 4 }));
+ assertThat(TestUtil.<ClientPutter>getPrivateField(insertToken, "clientPutter"), is(clientPutter));
+ verify(eventBus).post(any(ImageInsertStartedEvent.class));
+ }
+
+ @Test(expected = SoneInsertException.class)
+ public void insertExceptionCausesASoneException() throws InsertException, SoneException, IOException {
+ TemporaryImage temporaryImage = new TemporaryImage("image-id");
+ temporaryImage.setMimeType("image/png");
+ byte[] imageData = new byte[] { 1, 2, 3, 4 };
+ temporaryImage.setImageData(imageData);
+ Image image = new ImageImpl("image-id");
+ InsertToken insertToken = freenetInterface.new InsertToken(image);
+ InsertContext insertContext = mock(InsertContext.class);
+ when(highLevelSimpleClient.getInsertContext(anyBoolean())).thenReturn(insertContext);
+ ArgumentCaptor<InsertBlock> insertBlockCaptor = forClass(InsertBlock.class);
- callbackCaptor.getValue().onFoundEdition(3, key, null, null, false, (short) 0, null, true, true);
++ when(highLevelSimpleClient.insert(insertBlockCaptor.capture(), eq((String) null), eq(false), eq(insertContext), eq(insertToken), anyShort())).thenThrow(InsertException.class);
+ freenetInterface.insertImage(temporaryImage, image, insertToken);
+ }
+
+ @Test
+ public void insertingADirectory() throws InsertException, SoneException {
+ FreenetURI freenetUri = mock(FreenetURI.class);
+ HashMap<String, Object> manifestEntries = new HashMap<String, Object>();
+ String defaultFile = "index.html";
+ FreenetURI resultingUri = mock(FreenetURI.class);
+ when(highLevelSimpleClient.insertManifest(eq(freenetUri), eq(manifestEntries), eq(defaultFile))).thenReturn(resultingUri);
+ assertThat(freenetInterface.insertDirectory(freenetUri, manifestEntries, defaultFile), is(resultingUri));
+ }
+
+ @Test(expected = SoneException.class)
+ public void insertExceptionIsForwardedAsSoneException() throws InsertException, SoneException {
+ when(highLevelSimpleClient.insertManifest(any(FreenetURI.class), any(HashMap.class), any(String.class))).thenThrow(InsertException.class);
+ freenetInterface.insertDirectory(null, null, null);
+ }
+
+ @Test
+ public void soneWithWrongRequestUriWillNotBeSubscribed() throws MalformedURLException {
+ when(sone.getRequestUri()).thenReturn(new FreenetURI("KSK@GPLv3.txt"));
+ 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 registeringAUsk() {
+ FreenetURI freenetUri = createRandom(randomSource, "test-0").getURI().uskForSSK();
+ Callback callback = mock(Callback.class);
+ freenetInterface.registerUsk(freenetUri, callback);
+ verify(uskManager).subscribe(any(USK.class), any(USKCallback.class), anyBoolean(), eq((RequestClient) highLevelSimpleClient));
+ }
+
+ @Test
+ public void registeringANonUskKeyWillNotBeSubscribed() throws MalformedURLException {
+ FreenetURI freenetUri = new FreenetURI("KSK@GPLv3.txt");
+ Callback callback = mock(Callback.class);
+ freenetInterface.registerUsk(freenetUri, callback);
+ verify(uskManager, never()).subscribe(any(USK.class), any(USKCallback.class), anyBoolean(), eq((RequestClient) highLevelSimpleClient));
+ }
+
+ @Test
+ public void registeringAnActiveUskWillSubscribeToItCorrectly() {
+ FreenetURI freenetUri = createRandom(randomSource, "test-0").getURI().uskForSSK();
+ final USKCallback uskCallback = mock(USKCallback.class);
+ freenetInterface.registerActiveUsk(freenetUri, uskCallback);
+ verify(uskManager).subscribe(any(USK.class), eq(uskCallback), eq(true), any(RequestClient.class));
+ }
+
+ @Test
+ public void registeringAnInactiveUskWillSubscribeToItCorrectly() {
+ FreenetURI freenetUri = createRandom(randomSource, "test-0").getURI().uskForSSK();
+ final USKCallback uskCallback = mock(USKCallback.class);
+ freenetInterface.registerPassiveUsk(freenetUri, uskCallback);
+ verify(uskManager).subscribe(any(USK.class), eq(uskCallback), eq(false), any(RequestClient.class));
+ }
+
+ @Test
+ public void registeringAnActiveNonUskWillNotSubscribeToAUsk()
+ throws MalformedURLException {
+ FreenetURI freenetUri = createRandom(randomSource, "test-0").getURI();
+ freenetInterface.registerActiveUsk(freenetUri, null);
+ verify(uskManager, never()).subscribe(any(USK.class),
+ any(USKCallback.class), anyBoolean(),
+ eq((RequestClient) highLevelSimpleClient));
+ }
+
+ @Test
+ public void registeringAnInactiveNonUskWillNotSubscribeToAUsk()
+ throws MalformedURLException {
+ FreenetURI freenetUri = createRandom(randomSource, "test-0").getURI();
+ freenetInterface.registerPassiveUsk(freenetUri, null);
+ verify(uskManager, never()).subscribe(any(USK.class),
+ any(USKCallback.class), anyBoolean(),
+ eq((RequestClient) highLevelSimpleClient));
+ }
+
+ @Test
+ public void unregisteringANotRegisteredUskDoesNothing() {
+ FreenetURI freenetURI = createRandom(randomSource, "test-0").getURI().uskForSSK();
+ freenetInterface.unregisterUsk(freenetURI);
+ verify(uskManager, never()).unsubscribe(any(USK.class), any(USKCallback.class));
+ }
+
+ @Test
+ public void unregisteringARegisteredUsk() {
+ FreenetURI freenetURI = createRandom(randomSource, "test-0").getURI().uskForSSK();
+ Callback callback = mock(Callback.class);
+ freenetInterface.registerUsk(freenetURI, callback);
+ freenetInterface.unregisterUsk(freenetURI);
+ verify(uskManager).unsubscribe(any(USK.class), any(USKCallback.class));
+ }
+
+ @Test
+ public void unregisteringANotRegisteredSoneDoesNothing() {
+ freenetInterface.unregisterUsk(sone);
+ verify(uskManager, never()).unsubscribe(any(USK.class), any(USKCallback.class));
+ }
+
+ @Test
+ 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 {
+ 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 callbackForNormalUskUsesDifferentPriorities() {
+ Callback callback = mock(Callback.class);
+ 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));
+ }
+
+ @Test
+ public void callbackForNormalUskForwardsImportantParameters() throws MalformedURLException {
+ Callback callback = mock(Callback.class);
+ FreenetURI uri = createRandom(randomSource, "test-0").getURI().uskForSSK();
+ freenetInterface.registerUsk(uri, callback);
+ USK key = mock(USK.class);
+ when(key.getURI()).thenReturn(uri);
- insertToken.onFailure(null, null, null);
++ callbackCaptor.getValue().onFoundEdition(3, key, null, false, (short) 0, null, true, true);
+ verify(callback).editionFound(eq(uri), eq(3L), eq(true), eq(true));
+ }
+
+ @Test
+ public void fetchedRetainsUriAndFetchResult() {
+ FreenetURI freenetUri = mock(FreenetURI.class);
+ FetchResult fetchResult = mock(FetchResult.class);
+ Fetched fetched = new Fetched(freenetUri, fetchResult);
+ assertThat(fetched.getFreenetUri(), is(freenetUri));
+ assertThat(fetched.getFetchResult(), is(fetchResult));
+ }
+
+ @Test
+ public void cancellingAnInsertWillFireImageInsertAbortedEvent() {
+ ClientPutter clientPutter = mock(ClientPutter.class);
+ insertToken.setClientPutter(clientPutter);
+ ArgumentCaptor<ImageInsertStartedEvent> imageInsertStartedEvent = forClass(ImageInsertStartedEvent.class);
+ verify(eventBus).post(imageInsertStartedEvent.capture());
+ assertThat(imageInsertStartedEvent.getValue().image(), is(image));
+ insertToken.cancel();
+ ArgumentCaptor<ImageInsertAbortedEvent> imageInsertAbortedEvent = forClass(ImageInsertAbortedEvent.class);
+ verify(eventBus, times(2)).post(imageInsertAbortedEvent.capture());
++ verify(bucket).free();
+ assertThat(imageInsertAbortedEvent.getValue().image(), is(image));
+ }
+
+ @Test
+ public void failureWithoutExceptionSendsFailedEvent() {
- InsertException insertException = new InsertException(INTERNAL_ERROR, "Internal error", null);
- insertToken.onFailure(insertException, null, null);
++ insertToken.onFailure(null, null);
+ ArgumentCaptor<ImageInsertFailedEvent> imageInsertFailedEvent = forClass(ImageInsertFailedEvent.class);
+ verify(eventBus).post(imageInsertFailedEvent.capture());
++ verify(bucket).free();
+ assertThat(imageInsertFailedEvent.getValue().image(), is(image));
+ assertThat(imageInsertFailedEvent.getValue().cause(), nullValue());
+ }
+
+ @Test
+ public void failureSendsFailedEventWithException() {
- InsertException insertException = new InsertException(CANCELLED, null);
- insertToken.onFailure(insertException, null, null);
++ InsertException insertException = new InsertException(InsertExceptionMode.INTERNAL_ERROR, "Internal error", null);
++ insertToken.onFailure(insertException, null);
+ ArgumentCaptor<ImageInsertFailedEvent> imageInsertFailedEvent = forClass(ImageInsertFailedEvent.class);
+ verify(eventBus).post(imageInsertFailedEvent.capture());
++ verify(bucket).free();
+ assertThat(imageInsertFailedEvent.getValue().image(), is(image));
+ assertThat(imageInsertFailedEvent.getValue().cause(), is((Throwable) insertException));
+ }
+
+ @Test
+ public void failureBecauseCancelledByUserSendsAbortedEvent() {
- public void ignoredMethodsDoNotThrowExceptions() {
- insertToken.onMajorProgress(null);
- insertToken.onFetchable(null, null);
- insertToken.onGeneratedMetadata(null, null, null);
++ InsertException insertException = new InsertException(InsertExceptionMode.CANCELLED, null);
++ insertToken.onFailure(insertException, null);
+ ArgumentCaptor<ImageInsertAbortedEvent> imageInsertAbortedEvent = forClass(ImageInsertAbortedEvent.class);
+ verify(eventBus).post(imageInsertAbortedEvent.capture());
++ verify(bucket).free();
+ assertThat(imageInsertAbortedEvent.getValue().image(), is(image));
+ }
+
+ @Test
- insertToken.onGeneratedURI(generatedUri, null, null);
- insertToken.onSuccess(null, null);
++ public void ignoredMethodsDoNotThrowExceptions() throws ResumeFailedException {
++ insertToken.onResume(null);
++ insertToken.onFetchable(null);
++ insertToken.onGeneratedMetadata(null, null);
+ }
+
+ @Test
+ public void generatedUriIsPostedOnSuccess() {
+ FreenetURI generatedUri = mock(FreenetURI.class);
++ insertToken.onGeneratedURI(generatedUri, null);
++ insertToken.onSuccess(null);
+ ArgumentCaptor<ImageInsertFinishedEvent> imageInsertFinishedEvent = forClass(ImageInsertFinishedEvent.class);
+ verify(eventBus).post(imageInsertFinishedEvent.capture());
++ verify(bucket).free();
+ assertThat(imageInsertFinishedEvent.getValue().image(), is(image));
+ assertThat(imageInsertFinishedEvent.getValue().resultingUri(), is(generatedUri));
+ }
+
+ @Test
+ public void insertTokenSupplierSuppliesInsertTokens() {
+ InsertTokenSupplier insertTokenSupplier = freenetInterface.new InsertTokenSupplier();
+ assertThat(insertTokenSupplier.apply(image), notNullValue());
+ }
+
+ }
--- /dev/null
-import static org.hamcrest.Matchers.containsInAnyOrder;
+ package net.pterodactylus.sone.core;
+
+ import static com.google.common.base.Optional.of;
+ import static com.google.common.io.ByteStreams.toByteArray;
+ import static com.google.common.util.concurrent.MoreExecutors.sameThreadExecutor;
+ import static java.lang.System.currentTimeMillis;
+ import static org.hamcrest.MatcherAssert.assertThat;
-import net.pterodactylus.sone.core.SoneInserter.InsertInformation;
+ import static org.hamcrest.Matchers.containsString;
+ import static org.hamcrest.Matchers.instanceOf;
+ import static org.hamcrest.Matchers.is;
+ import static org.hamcrest.Matchers.nullValue;
+ import static org.mockito.Matchers.any;
+ import static org.mockito.Matchers.anyString;
+ import static org.mockito.Matchers.argThat;
+ import static org.mockito.Matchers.eq;
+ import static org.mockito.Mockito.doAnswer;
+ import static org.mockito.Mockito.mock;
+ import static org.mockito.Mockito.never;
+ import static org.mockito.Mockito.times;
+ import static org.mockito.Mockito.verify;
+ import static org.mockito.Mockito.when;
+
+ import java.io.IOException;
+ import java.util.HashMap;
+ import java.util.Map;
+
-import freenet.client.async.ManifestElement;
+ import net.pterodactylus.sone.core.SoneInserter.ManifestCreator;
+ import net.pterodactylus.sone.core.event.InsertionDelayChangedEvent;
+ import net.pterodactylus.sone.core.event.SoneEvent;
+ import net.pterodactylus.sone.core.event.SoneInsertAbortedEvent;
+ import net.pterodactylus.sone.core.event.SoneInsertedEvent;
+ import net.pterodactylus.sone.core.event.SoneInsertingEvent;
+ import net.pterodactylus.sone.data.Album;
+ import net.pterodactylus.sone.data.Sone;
+ import net.pterodactylus.sone.main.SonePlugin;
+
+ import freenet.keys.FreenetURI;
++import freenet.support.api.ManifestElement;
+
+ import com.google.common.base.Charsets;
+ import com.google.common.base.Optional;
+ import com.google.common.eventbus.AsyncEventBus;
+ 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 SoneInserter} and its subclasses.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ public class SoneInserterTest {
+
+ private final Core core = mock(Core.class);
+ private final EventBus eventBus = mock(EventBus.class);
+ private final FreenetInterface freenetInterface = mock(FreenetInterface.class);
+
+ @Before
+ public void setupCore() {
+ UpdateChecker updateChecker = mock(UpdateChecker.class);
+ when(core.getUpdateChecker()).thenReturn(updateChecker);
+ when(core.getSone(anyString())).thenReturn(Optional.<Sone>absent());
+ }
+
+ @Test
+ public void insertionDelayIsForwardedToSoneInserter() {
+ EventBus eventBus = new AsyncEventBus(sameThreadExecutor());
+ eventBus.register(new SoneInserter(core, eventBus, freenetInterface, "SoneId"));
+ eventBus.post(new InsertionDelayChangedEvent(15));
+ assertThat(SoneInserter.getInsertionDelay().get(), is(15));
+ }
+
+ private Sone createSone(FreenetURI insertUri, String fingerprint) {
+ Sone sone = mock(Sone.class);
+ when(sone.getInsertUri()).thenReturn(insertUri);
+ when(sone.getFingerprint()).thenReturn(fingerprint);
+ when(sone.getRootAlbum()).thenReturn(mock(Album.class));
+ when(core.getSone(anyString())).thenReturn(of(sone));
+ return sone;
+ }
+
+ @Test
+ public void isModifiedIsTrueIfModificationDetectorSaysSo() {
+ SoneModificationDetector soneModificationDetector = mock(SoneModificationDetector.class);
+ when(soneModificationDetector.isModified()).thenReturn(true);
+ SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId", soneModificationDetector, 1);
+ assertThat(soneInserter.isModified(), is(true));
+ }
+
+ @Test
+ public void isModifiedIsFalseIfModificationDetectorSaysSo() {
+ SoneModificationDetector soneModificationDetector = mock(SoneModificationDetector.class);
+ SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId", soneModificationDetector, 1);
+ assertThat(soneInserter.isModified(), is(false));
+ }
+
+ @Test
+ public void lastFingerprintIsStoredCorrectly() {
+ SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId");
+ soneInserter.setLastInsertFingerprint("last-fingerprint");
+ assertThat(soneInserter.getLastInsertFingerprint(), is("last-fingerprint"));
+ }
+
+ @Test
+ public void soneInserterStopsWhenItShould() {
+ SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId");
+ soneInserter.stop();
+ soneInserter.serviceRun();
+ }
+
+ @Test
+ public void soneInserterInsertsASoneIfItIsEligible() throws SoneException {
+ FreenetURI insertUri = mock(FreenetURI.class);
+ final FreenetURI finalUri = mock(FreenetURI.class);
+ String fingerprint = "fingerprint";
+ Sone sone = createSone(insertUri, fingerprint);
+ SoneModificationDetector soneModificationDetector = mock(SoneModificationDetector.class);
+ when(soneModificationDetector.isEligibleForInsert()).thenReturn(true);
+ when(freenetInterface.insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html"))).thenReturn(finalUri);
+ final SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId", soneModificationDetector, 1);
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) throws Throwable {
+ soneInserter.stop();
+ return null;
+ }
+ }).when(core).touchConfiguration();
+ soneInserter.serviceRun();
+ ArgumentCaptor<SoneEvent> soneEvents = ArgumentCaptor.forClass(SoneEvent.class);
+ verify(freenetInterface).insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html"));
+ verify(eventBus, times(2)).post(soneEvents.capture());
+ assertThat(soneEvents.getAllValues().get(0), instanceOf(SoneInsertingEvent.class));
+ assertThat(soneEvents.getAllValues().get(0).sone(), is(sone));
+ assertThat(soneEvents.getAllValues().get(1), instanceOf(SoneInsertedEvent.class));
+ assertThat(soneEvents.getAllValues().get(1).sone(), is(sone));
+ }
+
+ @Test
+ public void soneInserterBailsOutIfItIsStoppedWhileInserting() throws SoneException {
+ FreenetURI insertUri = mock(FreenetURI.class);
+ final FreenetURI finalUri = mock(FreenetURI.class);
+ String fingerprint = "fingerprint";
+ Sone sone = createSone(insertUri, fingerprint);
+ SoneModificationDetector soneModificationDetector = mock(SoneModificationDetector.class);
+ when(soneModificationDetector.isEligibleForInsert()).thenReturn(true);
+ final SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId", soneModificationDetector, 1);
+ when(freenetInterface.insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html"))).thenAnswer(new Answer<FreenetURI>() {
+ @Override
+ public FreenetURI answer(InvocationOnMock invocation) throws Throwable {
+ soneInserter.stop();
+ return finalUri;
+ }
+ });
+ soneInserter.serviceRun();
+ ArgumentCaptor<SoneEvent> soneEvents = ArgumentCaptor.forClass(SoneEvent.class);
+ verify(freenetInterface).insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html"));
+ verify(eventBus, times(2)).post(soneEvents.capture());
+ assertThat(soneEvents.getAllValues().get(0), instanceOf(SoneInsertingEvent.class));
+ assertThat(soneEvents.getAllValues().get(0).sone(), is(sone));
+ assertThat(soneEvents.getAllValues().get(1), instanceOf(SoneInsertedEvent.class));
+ assertThat(soneEvents.getAllValues().get(1).sone(), is(sone));
+ verify(core, never()).touchConfiguration();
+ }
+
+ @Test
+ public void soneInserterDoesNotInsertSoneIfItIsNotEligible() throws SoneException {
+ FreenetURI insertUri = mock(FreenetURI.class);
+ String fingerprint = "fingerprint";
+ Sone sone = createSone(insertUri, fingerprint);
+ SoneModificationDetector soneModificationDetector = mock(SoneModificationDetector.class);
+ final SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId", soneModificationDetector, 1);
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException ie1) {
+ throw new RuntimeException(ie1);
+ }
+ soneInserter.stop();
+ }
+ }).start();
+ soneInserter.serviceRun();
+ verify(freenetInterface, never()).insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html"));
+ verify(eventBus, never()).post(argThat(org.hamcrest.Matchers.any(SoneEvent.class)));
+ }
+
+ @Test
+ public void soneInserterPostsAbortedEventIfAnExceptionOccurs() throws SoneException {
+ FreenetURI insertUri = mock(FreenetURI.class);
+ String fingerprint = "fingerprint";
+ Sone sone = createSone(insertUri, fingerprint);
+ SoneModificationDetector soneModificationDetector = mock(SoneModificationDetector.class);
+ when(soneModificationDetector.isEligibleForInsert()).thenReturn(true);
+ final SoneInserter soneInserter = new SoneInserter(core, eventBus, freenetInterface, "SoneId", soneModificationDetector, 1);
+ final SoneException soneException = new SoneException(new Exception());
+ when(freenetInterface.insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html"))).thenAnswer(new Answer<FreenetURI>() {
+ @Override
+ public FreenetURI answer(InvocationOnMock invocation) throws Throwable {
+ soneInserter.stop();
+ throw soneException;
+ }
+ });
+ soneInserter.serviceRun();
+ ArgumentCaptor<SoneEvent> soneEvents = ArgumentCaptor.forClass(SoneEvent.class);
+ verify(freenetInterface).insertDirectory(eq(insertUri), any(HashMap.class), eq("index.html"));
+ verify(eventBus, times(2)).post(soneEvents.capture());
+ assertThat(soneEvents.getAllValues().get(0), instanceOf(SoneInsertingEvent.class));
+ assertThat(soneEvents.getAllValues().get(0).sone(), is(sone));
+ assertThat(soneEvents.getAllValues().get(1), instanceOf(SoneInsertAbortedEvent.class));
+ assertThat(soneEvents.getAllValues().get(1).sone(), is(sone));
+ verify(core, never()).touchConfiguration();
+ }
+
+ @Test
+ public void soneInserterExitsIfSoneIsUnknown() {
+ SoneModificationDetector soneModificationDetector =
+ mock(SoneModificationDetector.class);
+ SoneInserter soneInserter =
+ new SoneInserter(core, eventBus, freenetInterface, "SoneId",
+ soneModificationDetector, 1);
+ when(soneModificationDetector.isEligibleForInsert()).thenReturn(true);
+ when(core.getSone("SoneId")).thenReturn(Optional.<Sone>absent());
+ soneInserter.serviceRun();
+ }
+
+ @Test
+ public void soneInserterCatchesExceptionAndContinues() {
+ SoneModificationDetector soneModificationDetector =
+ mock(SoneModificationDetector.class);
+ final SoneInserter soneInserter =
+ new SoneInserter(core, eventBus, freenetInterface, "SoneId",
+ soneModificationDetector, 1);
+ Answer<Optional<Sone>> stopInserterAndThrowException =
+ new Answer<Optional<Sone>>() {
+ @Override
+ public Optional<Sone> answer(
+ InvocationOnMock invocation) {
+ soneInserter.stop();
+ throw new NullPointerException();
+ }
+ };
+ when(soneModificationDetector.isEligibleForInsert()).thenAnswer(
+ stopInserterAndThrowException);
+ soneInserter.serviceRun();
+ }
+
+ @Test
+ public void templateIsRenderedCorrectlyForManifestElement()
+ throws IOException {
+ Map<String, Object> soneProperties = new HashMap<String, Object>();
+ soneProperties.put("id", "SoneId");
+ ManifestCreator manifestCreator = new ManifestCreator(core, soneProperties);
+ long now = currentTimeMillis();
+ when(core.getStartupTime()).thenReturn(now);
+ ManifestElement manifestElement = manifestCreator.createManifestElement("test.txt", "plain/text; charset=utf-8", "sone-inserter-manifest.txt");
+ assertThat(manifestElement.getName(), is("test.txt"));
+ assertThat(manifestElement.getMimeTypeOverride(), is("plain/text; charset=utf-8"));
+ String templateContent = new String(toByteArray(manifestElement.getData().getInputStream()), Charsets.UTF_8);
+ assertThat(templateContent, containsString("Sone Version: " + SonePlugin.VERSION.toString() + "\n"));
+ assertThat(templateContent, containsString("Core Startup: " + now + "\n"));
+ assertThat(templateContent, containsString("Sone ID: " + "SoneId" + "\n"));
+ }
+
+ @Test
+ public void invalidTemplateReturnsANullManifestElement() {
+ Map<String, Object> soneProperties = new HashMap<String, Object>();
+ ManifestCreator manifestCreator = new ManifestCreator(core, soneProperties);
+ assertThat(manifestCreator.createManifestElement("test.txt",
+ "plain/text; charset=utf-8",
+ "sone-inserter-invalid-manifest.txt"),
+ nullValue());
+ }
+
+ @Test
+ public void errorWhileRenderingTemplateReturnsANullManifestElement() {
+ Map<String, Object> soneProperties = new HashMap<String, Object>();
+ ManifestCreator manifestCreator = new ManifestCreator(core, soneProperties);
+ when(core.toString()).thenThrow(NullPointerException.class);
+ assertThat(manifestCreator.createManifestElement("test.txt",
+ "plain/text; charset=utf-8",
+ "sone-inserter-faulty-manifest.txt"),
+ nullValue());
+ }
+
+ }
--- /dev/null
-import net.pterodactylus.sone.freenet.StringBucket;
+ package net.pterodactylus.sone.core;
+
+ import static java.lang.Long.MAX_VALUE;
+ import static net.pterodactylus.sone.main.SonePlugin.VERSION;
+ import static org.hamcrest.MatcherAssert.assertThat;
+ import static org.hamcrest.Matchers.instanceOf;
+ import static org.hamcrest.Matchers.is;
+ import static org.mockito.ArgumentCaptor.forClass;
+ import static org.mockito.Matchers.any;
+ import static org.mockito.Matchers.argThat;
+ import static org.mockito.Mockito.mock;
+ import static org.mockito.Mockito.never;
+ import static org.mockito.Mockito.times;
+ import static org.mockito.Mockito.verify;
+ import static org.mockito.Mockito.when;
+
+ import java.io.IOException;
+ import java.io.InputStream;
+
+ import net.pterodactylus.sone.core.FreenetInterface.Callback;
+ import net.pterodactylus.sone.core.FreenetInterface.Fetched;
+ import net.pterodactylus.sone.core.event.UpdateFoundEvent;
- Bucket fetched = new StringBucket("# MapConfigurationBackendVersion=1\n" +
+ import net.pterodactylus.util.version.Version;
+
+ import freenet.client.ClientMetadata;
+ import freenet.client.FetchResult;
+ import freenet.keys.FreenetURI;
+ import freenet.support.api.Bucket;
++import freenet.support.io.ArrayBucket;
+
+ 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 UpdateChecker}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ public class UpdateCheckerTest {
+
+ private final EventBus eventBus = mock(EventBus.class);
+ private final FreenetInterface freenetInterface = mock(FreenetInterface.class);
+ private final UpdateChecker updateChecker = new UpdateChecker(eventBus, freenetInterface);
+
+ @Before
+ public void startUpdateChecker() {
+ updateChecker.start();
+ }
+
+ @Test
+ public void newUpdateCheckerDoesNotHaveALatestVersion() {
+ assertThat(updateChecker.hasLatestVersion(), is(false));
+ assertThat(updateChecker.getLatestVersion(), is(VERSION));
+ }
+
+ @Test
+ public void startingAnUpdateCheckerRegisterAUsk() {
+ verify(freenetInterface).registerUsk(any(FreenetURI.class), any(Callback.class));
+ }
+
+ @Test
+ public void stoppingAnUpdateCheckerUnregistersAUsk() {
+ updateChecker.stop();
+ verify(freenetInterface).unregisterUsk(any(FreenetURI.class));
+ }
+
+ @Test
+ public void callbackDoesNotDownloadIfNewEditionIsNotFound() {
+ setupCallbackWithEdition(MAX_VALUE, false, false);
+ verify(freenetInterface, never()).fetchUri(any(FreenetURI.class));
+ verify(eventBus, never()).post(argThat(instanceOf(UpdateFoundEvent.class)));
+ }
+
+ private void setupCallbackWithEdition(long edition, boolean newKnownGood, boolean newSlot) {
+ ArgumentCaptor<FreenetURI> uri = forClass(FreenetURI.class);
+ ArgumentCaptor<Callback> callback = forClass(Callback.class);
+ verify(freenetInterface).registerUsk(uri.capture(), callback.capture());
+ callback.getValue().editionFound(uri.getValue(), edition, newKnownGood, newSlot);
+ }
+
+ @Test
+ public void callbackStartsIfNewEditionIsFound() {
+ setupFetchResult(createFutureFetchResult());
+ setupCallbackWithEdition(MAX_VALUE, true, false);
+ verifyAFreenetUriIsFetched();
+ ArgumentCaptor<UpdateFoundEvent> updateFoundEvent = forClass(UpdateFoundEvent.class);
+ verify(eventBus, times(1)).post(updateFoundEvent.capture());
+ assertThat(updateFoundEvent.getValue().version(), is(new Version(99, 0, 0)));
+ assertThat(updateFoundEvent.getValue().releaseTime(), is(11865368297000L));
+ assertThat(updateChecker.getLatestVersion(), is(new Version(99, 0, 0)));
+ assertThat(updateChecker.getLatestVersionDate(), is(11865368297000L));
+ assertThat(updateChecker.hasLatestVersion(), is(true));
+ }
+
+ private FetchResult createFutureFetchResult() {
+ ClientMetadata clientMetadata = new ClientMetadata("application/xml");
- "CurrentVersion/ReleaseTime: 11865368297000");
++ Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" +
+ "CurrentVersion/Version: 99.0.0\n" +
- Bucket fetched = new StringBucket("# MapConfigurationBackendVersion=1\n" +
++ "CurrentVersion/ReleaseTime: 11865368297000").getBytes());
+ return new FetchResult(clientMetadata, fetched);
+ }
+
+ @Test
+ public void callbackDoesNotStartIfNoNewEditionIsFound() {
+ setupFetchResult(createPastFetchResult());
+ setupCallbackWithEdition(updateChecker.getLatestEdition(), true, false);
+ verifyAFreenetUriIsFetched();
+ verifyNoUpdateFoundEventIsFired();
+ }
+
+ private void setupFetchResult(final FetchResult pastFetchResult) {
+ when(freenetInterface.fetchUri(any(FreenetURI.class))).thenAnswer(new Answer<Fetched>() {
+ @Override
+ public Fetched answer(InvocationOnMock invocation) throws Throwable {
+ FreenetURI freenetUri = (FreenetURI) invocation.getArguments()[0];
+ return new Fetched(freenetUri, pastFetchResult);
+ }
+ });
+ }
+
+ private FetchResult createPastFetchResult() {
+ ClientMetadata clientMetadata = new ClientMetadata("application/xml");
- "CurrentVersion/ReleaseTime: 1289417883000");
++ Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" +
+ "CurrentVersion/Version: 0.2\n" +
- Bucket fetched = new StringBucket("Some other data.");
++ "CurrentVersion/ReleaseTime: 1289417883000").getBytes());
+ return new FetchResult(clientMetadata, fetched);
+ }
+
+ @Test
+ public void invalidUpdateFileDoesNotStartCallback() {
+ setupFetchResult(createInvalidFetchResult());
+ setupCallbackWithEdition(MAX_VALUE, true, false);
+ verifyAFreenetUriIsFetched();
+ verifyNoUpdateFoundEventIsFired();
+ }
+
+ private FetchResult createInvalidFetchResult() {
+ ClientMetadata clientMetadata = new ClientMetadata("text/plain");
- Bucket fetched = new StringBucket("Some other data.") {
++ Bucket fetched = new ArrayBucket("Some other data.".getBytes());
+ return new FetchResult(clientMetadata, fetched);
+ }
+
+ @Test
+ public void nonExistingPropertiesWillNotCauseUpdateToBeFound() {
+ setupCallbackWithEdition(MAX_VALUE, true, false);
+ verifyAFreenetUriIsFetched();
+ verifyNoUpdateFoundEventIsFired();
+ }
+
+ private void verifyNoUpdateFoundEventIsFired() {
+ verify(eventBus, never()).post(any(UpdateFoundEvent.class));
+ }
+
+ private void verifyAFreenetUriIsFetched() {
+ verify(freenetInterface).fetchUri(any(FreenetURI.class));
+ }
+
+ @Test
+ public void brokenBucketDoesNotCauseUpdateToBeFound() {
+ setupFetchResult(createBrokenBucketFetchResult());
+ setupCallbackWithEdition(MAX_VALUE, true, false);
+ verifyAFreenetUriIsFetched();
+ verifyNoUpdateFoundEventIsFired();
+ }
+
+ private FetchResult createBrokenBucketFetchResult() {
+ ClientMetadata clientMetadata = new ClientMetadata("text/plain");
- Bucket fetched = new StringBucket("# MapConfigurationBackendVersion=1\n" +
++ Bucket fetched = new ArrayBucket("Some other data.".getBytes()) {
+ @Override
+ public InputStream getInputStream() {
+ try {
+ return when(mock(InputStream.class).read()).thenThrow(IOException.class).getMock();
+ } catch (IOException ioe1) {
+ /* won’t throw here. */
+ return null;
+ }
+ }
+ };
+ return new FetchResult(clientMetadata, fetched);
+ }
+
+ @Test
+ public void invalidTimeDoesNotCauseAnUpdateToBeFound() {
+ setupFetchResult(createInvalidTimeFetchResult());
+ setupCallbackWithEdition(MAX_VALUE, true, false);
+ verifyAFreenetUriIsFetched();
+ verifyNoUpdateFoundEventIsFired();
+ }
+
+ private FetchResult createInvalidTimeFetchResult() {
+ ClientMetadata clientMetadata = new ClientMetadata("application/xml");
- "CurrentVersion/ReleaseTime: invalid");
++ Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" +
+ "CurrentVersion/Version: 0.2\n" +
- Bucket fetched = new StringBucket("# MapConfigurationBackendVersion=1\n" +
- "CurrentVersion/Version: 0.2\n");
++ "CurrentVersion/ReleaseTime: invalid").getBytes());
+ return new FetchResult(clientMetadata, fetched);
+ }
+
+ @Test
+ public void invalidPropertiesDoesNotCauseAnUpdateToBeFound() {
+ setupFetchResult(createMissingTimeFetchResult());
+ setupCallbackWithEdition(MAX_VALUE, true, false);
+ verifyAFreenetUriIsFetched();
+ verifyNoUpdateFoundEventIsFired();
+ }
+
+ private FetchResult createMissingTimeFetchResult() {
+ ClientMetadata clientMetadata = new ClientMetadata("application/xml");
- Bucket fetched = new StringBucket("# MapConfigurationBackendVersion=1\n" +
++ Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" +
++ "CurrentVersion/Version: 0.2\n").getBytes());
+ return new FetchResult(clientMetadata, fetched);
+ }
+
+ @Test
+ public void invalidVersionDoesNotCauseAnUpdateToBeFound() {
+ setupFetchResult(createInvalidVersionFetchResult());
+ setupCallbackWithEdition(MAX_VALUE, true, false);
+ verifyAFreenetUriIsFetched();
+ verifyNoUpdateFoundEventIsFired();
+ }
+
+ private FetchResult createInvalidVersionFetchResult() {
+ ClientMetadata clientMetadata = new ClientMetadata("application/xml");
- "CurrentVersion/ReleaseTime: 1289417883000");
++ Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" +
+ "CurrentVersion/Version: foo\n" +
++ "CurrentVersion/ReleaseTime: 1289417883000").getBytes());
+ return new FetchResult(clientMetadata, fetched);
+ }
+
+ }