From: David ‘Bombe’ Roden Date: Fri, 12 Jun 2015 18:47:55 +0000 (+0200) Subject: Merge branch 'last-working' into next X-Git-Tag: 0.9-rc1^2~3 X-Git-Url: https://git.pterodactylus.net/?p=Sone.git;a=commitdiff_plain;h=f229fe41f708d2b275c20ceb9aba5993761218a3 Merge branch 'last-working' into next This is the current state with lots and lots of changes since the last release. I have been using this state during the last year and it should work pretty much the same as before (minus a couple of bugs). Conflicts: src/main/java/net/pterodactylus/sone/core/FreenetInterface.java src/main/java/net/pterodactylus/sone/core/SoneInserter.java src/main/java/net/pterodactylus/sone/freenet/PluginStoreConfigurationBackend.java --- f229fe41f708d2b275c20ceb9aba5993761218a3 diff --cc pom.xml index 4309f05,0dfc383..67e29c9 --- a/pom.xml +++ b/pom.xml @@@ -22,9 -22,14 +22,14 @@@ test + org.hamcrest + hamcrest-all + 1.3 + + org.freenetproject fred - 0.7.5.1405 + 0.7.5.1467.99.3 provided diff --cc src/main/java/net/pterodactylus/sone/core/FreenetInterface.java index 4ee3926,dc0e3ea..e802ba2 --- a/src/main/java/net/pterodactylus/sone/core/FreenetInterface.java +++ b/src/main/java/net/pterodactylus/sone/core/FreenetInterface.java @@@ -32,17 -37,18 +37,17 @@@ import net.pterodactylus.sone.core.even 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; @@@ -129,11 -122,10 +135,10 @@@ public class FreenetInterface 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; } @@@ -462,8 -417,8 +436,9 @@@ */ @SuppressWarnings("synthetic-access") public void cancel() { - clientPutter.cancel(null, node.clientCore.clientContext); + clientPutter.cancel(node.clientCore.clientContext); eventBus.post(new ImageInsertAbortedEvent(image)); ++ bucket.free(); } // diff --cc src/main/java/net/pterodactylus/sone/core/SoneDownloaderImpl.java index 0000000,ff0eaf7..b36c2d3 mode 000000,100644..100644 --- a/src/main/java/net/pterodactylus/sone/core/SoneDownloaderImpl.java +++ b/src/main/java/net/pterodactylus/sone/core/SoneDownloaderImpl.java @@@ -1,0 -1,275 +1,274 @@@ + /* + * 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 . + */ + + 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 David ‘Bombe’ Roden + */ + 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 sones = new HashSet(); + + /** + * 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, - 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) { + 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); + } + } + + } diff --cc src/main/java/net/pterodactylus/sone/core/SoneInserter.java index c5b9e40,33bc240..86bf049 --- a/src/main/java/net/pterodactylus/sone/core/SoneInserter.java +++ b/src/main/java/net/pterodactylus/sone/core/SoneInserter.java @@@ -17,16 -17,19 +17,21 @@@ 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; @@@ -38,9 -43,9 +45,8 @@@ import net.pterodactylus.sone.data.Post 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; @@@ -51,16 -56,15 +57,19 @@@ import net.pterodactylus.util.template. 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. @@@ -292,11 -279,11 +285,13 @@@ public class SoneInserter extends Abstr * * @author David ‘Bombe’ Roden */ - private class InsertInformation { + @VisibleForTesting - class InsertInformation { ++ class InsertInformation implements Closeable { + /** All properties of the Sone, copied for thread safety. */ + private final Map soneProperties = new HashMap(); - private final Set buckets = new HashSet(); + private final String fingerprint; + private final ManifestCreator manifestCreator; /** * Creates a new insert information container. @@@ -362,27 -341,31 +349,37 @@@ 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 David ‘Bombe’ Roden + */ + @VisibleForTesting - static class ManifestCreator { ++ static class ManifestCreator implements Closeable { + + private final Core core; + private final Map soneProperties; ++ private final Set buckets = new HashSet(); + + ManifestCreator(Core core, Map 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); diff --cc src/main/java/net/pterodactylus/sone/freenet/PluginStoreConfigurationBackend.java index 5bde0b0,0ed6dd5..eb16a09 --- a/src/main/java/net/pterodactylus/sone/freenet/PluginStoreConfigurationBackend.java +++ b/src/main/java/net/pterodactylus/sone/freenet/PluginStoreConfigurationBackend.java @@@ -23,8 -25,7 +25,7 @@@ import net.pterodactylus.util.config.At 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; diff --cc src/test/java/net/pterodactylus/sone/core/FreenetInterfaceTest.java index 0000000,c753740..091c93f mode 000000,100644..100644 --- a/src/test/java/net/pterodactylus/sone/core/FreenetInterfaceTest.java +++ b/src/test/java/net/pterodactylus/sone/core/FreenetInterfaceTest.java @@@ -1,0 -1,393 +1,401 @@@ + package net.pterodactylus.sone.core; + -import static freenet.client.InsertException.CANCELLED; -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 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; -import net.pterodactylus.sone.freenet.StringBucket; + + 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 David ‘Bombe’ Roden + */ + 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 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.PERMANENT_REDIRECT, newFreenetUri); ++ 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"); - FetchException fetchException = new FetchException(FetchException.ALL_DATA_NOT_FOUND); ++ 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"); - Bucket bucket = new StringBucket("Some Data."); ++ 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 insertBlockCaptor = forClass(InsertBlock.class); - when(highLevelSimpleClient.insert(insertBlockCaptor.capture(), eq(false), eq((String) null), eq(false), eq(insertContext), eq(insertToken), anyShort())).thenReturn(clientPutter); ++ 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.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 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())).thenThrow(InsertException.class); + freenetInterface.insertImage(temporaryImage, image, insertToken); + } + + @Test + public void insertingADirectory() throws InsertException, SoneException { + FreenetURI freenetUri = mock(FreenetURI.class); + HashMap manifestEntries = new HashMap(); + 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); - callbackCaptor.getValue().onFoundEdition(3, key, null, null, false, (short) 0, null, true, true); ++ 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 = forClass(ImageInsertStartedEvent.class); + verify(eventBus).post(imageInsertStartedEvent.capture()); + assertThat(imageInsertStartedEvent.getValue().image(), is(image)); + insertToken.cancel(); + ArgumentCaptor imageInsertAbortedEvent = forClass(ImageInsertAbortedEvent.class); + verify(eventBus, times(2)).post(imageInsertAbortedEvent.capture()); ++ verify(bucket).free(); + assertThat(imageInsertAbortedEvent.getValue().image(), is(image)); + } + + @Test + public void failureWithoutExceptionSendsFailedEvent() { - insertToken.onFailure(null, null, null); ++ insertToken.onFailure(null, null); + ArgumentCaptor 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(INTERNAL_ERROR, "Internal error", null); - insertToken.onFailure(insertException, null, null); ++ InsertException insertException = new InsertException(InsertExceptionMode.INTERNAL_ERROR, "Internal error", null); ++ insertToken.onFailure(insertException, null); + ArgumentCaptor 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() { - InsertException insertException = new InsertException(CANCELLED, null); - insertToken.onFailure(insertException, null, null); ++ InsertException insertException = new InsertException(InsertExceptionMode.CANCELLED, null); ++ insertToken.onFailure(insertException, null); + ArgumentCaptor imageInsertAbortedEvent = forClass(ImageInsertAbortedEvent.class); + verify(eventBus).post(imageInsertAbortedEvent.capture()); ++ verify(bucket).free(); + assertThat(imageInsertAbortedEvent.getValue().image(), is(image)); + } + + @Test - public void ignoredMethodsDoNotThrowExceptions() { - insertToken.onMajorProgress(null); - insertToken.onFetchable(null, null); - insertToken.onGeneratedMetadata(null, 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, null); - insertToken.onSuccess(null, null); ++ insertToken.onGeneratedURI(generatedUri, null); ++ insertToken.onSuccess(null); + ArgumentCaptor 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()); + } + + } diff --cc src/test/java/net/pterodactylus/sone/core/SoneInserterTest.java index 0000000,e0ff3a5..552746e mode 000000,100644..100644 --- a/src/test/java/net/pterodactylus/sone/core/SoneInserterTest.java +++ b/src/test/java/net/pterodactylus/sone/core/SoneInserterTest.java @@@ -1,0 -1,291 +1,289 @@@ + 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 static org.hamcrest.Matchers.containsInAnyOrder; + 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 net.pterodactylus.sone.core.SoneInserter.InsertInformation; + 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.client.async.ManifestElement; + 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 David ‘Bombe’ Roden + */ + 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.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() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + soneInserter.stop(); + return null; + } + }).when(core).touchConfiguration(); + soneInserter.serviceRun(); + ArgumentCaptor 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() { + @Override + public FreenetURI answer(InvocationOnMock invocation) throws Throwable { + soneInserter.stop(); + return finalUri; + } + }); + soneInserter.serviceRun(); + ArgumentCaptor 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() { + @Override + public FreenetURI answer(InvocationOnMock invocation) throws Throwable { + soneInserter.stop(); + throw soneException; + } + }); + soneInserter.serviceRun(); + ArgumentCaptor 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.absent()); + soneInserter.serviceRun(); + } + + @Test + public void soneInserterCatchesExceptionAndContinues() { + SoneModificationDetector soneModificationDetector = + mock(SoneModificationDetector.class); + final SoneInserter soneInserter = + new SoneInserter(core, eventBus, freenetInterface, "SoneId", + soneModificationDetector, 1); + Answer> stopInserterAndThrowException = + new Answer>() { + @Override + public Optional answer( + InvocationOnMock invocation) { + soneInserter.stop(); + throw new NullPointerException(); + } + }; + when(soneModificationDetector.isEligibleForInsert()).thenAnswer( + stopInserterAndThrowException); + soneInserter.serviceRun(); + } + + @Test + public void templateIsRenderedCorrectlyForManifestElement() + throws IOException { + Map soneProperties = new HashMap(); + 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 soneProperties = new HashMap(); + 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 soneProperties = new HashMap(); + 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()); + } + + } diff --cc src/test/java/net/pterodactylus/sone/core/UpdateCheckerTest.java index 0000000,b3f8c08..a5e3b2a mode 000000,100644..100644 --- a/src/test/java/net/pterodactylus/sone/core/UpdateCheckerTest.java +++ b/src/test/java/net/pterodactylus/sone/core/UpdateCheckerTest.java @@@ -1,0 -1,233 +1,233 @@@ + 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; -import net.pterodactylus.sone.freenet.StringBucket; + 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 David ‘Bombe’ Roden + */ + 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 uri = forClass(FreenetURI.class); + ArgumentCaptor 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 = 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"); - Bucket fetched = new StringBucket("# MapConfigurationBackendVersion=1\n" + ++ Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" + + "CurrentVersion/Version: 99.0.0\n" + - "CurrentVersion/ReleaseTime: 11865368297000"); ++ "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() { + @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"); - Bucket fetched = new StringBucket("# MapConfigurationBackendVersion=1\n" + ++ Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" + + "CurrentVersion/Version: 0.2\n" + - "CurrentVersion/ReleaseTime: 1289417883000"); ++ "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("Some other data.") { ++ 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"); - Bucket fetched = new StringBucket("# MapConfigurationBackendVersion=1\n" + ++ Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" + + "CurrentVersion/Version: 0.2\n" + - "CurrentVersion/ReleaseTime: invalid"); ++ "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" + - "CurrentVersion/Version: 0.2\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"); - Bucket fetched = new StringBucket("# MapConfigurationBackendVersion=1\n" + ++ Bucket fetched = new ArrayBucket(("# MapConfigurationBackendVersion=1\n" + + "CurrentVersion/Version: foo\n" + - "CurrentVersion/ReleaseTime: 1289417883000"); ++ "CurrentVersion/ReleaseTime: 1289417883000").getBytes()); + return new FetchResult(clientMetadata, fetched); + } + + }