From 722b47810ffbe01465f104791c9f660ae161023b Mon Sep 17 00:00:00 2001 From: =?utf8?q?David=20=E2=80=98Bombe=E2=80=99=20Roden?= Date: Fri, 5 Dec 2014 22:19:46 +0100 Subject: [PATCH] Move management of Sone following times to database. --- .../java/net/pterodactylus/sone/core/Core.java | 73 ++------- .../sone/database/FriendProvider.java | 3 + .../sone/database/memory/ConfigurationLoader.java | 34 +++++ .../sone/database/memory/MemoryDatabase.java | 7 + .../sone/database/memory/MemoryFriendDatabase.java | 49 +++++++ .../sone/database/memory/MemoryDatabaseTest.java | 163 +++++++++++++-------- 6 files changed, 210 insertions(+), 119 deletions(-) diff --git a/src/main/java/net/pterodactylus/sone/core/Core.java b/src/main/java/net/pterodactylus/sone/core/Core.java index 6ca7372..291cb95 100644 --- a/src/main/java/net/pterodactylus/sone/core/Core.java +++ b/src/main/java/net/pterodactylus/sone/core/Core.java @@ -143,9 +143,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, /** The trust updater. */ private final WebOfTrustUpdater webOfTrustUpdater; - /** The times Sones were followed. */ - private final Map soneFollowingTimes = new HashMap(); - /** Locked local Sones. */ /* synchronize on itself. */ private final Set lockedSones = new HashSet(); @@ -373,9 +370,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, * been followed, or {@link Long#MAX_VALUE} */ public long getSoneFollowingTime(Sone sone) { - synchronized (soneFollowingTimes) { - return Optional.fromNullable(soneFollowingTimes.get(sone.getId())).or(Long.MAX_VALUE); - } + return database.getSoneFollowingTime(sone.getId()).or(Long.MAX_VALUE); } /** @@ -694,28 +689,25 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, public void followSone(LocalSone sone, String soneId) { checkNotNull(sone, "sone must not be null"); checkNotNull(soneId, "soneId must not be null"); + boolean newFriend = !database.getSoneFollowingTime(soneId).isPresent(); database.addFriend(sone, soneId); - synchronized (soneFollowingTimes) { - if (!soneFollowingTimes.containsKey(soneId)) { - long now = System.currentTimeMillis(); - soneFollowingTimes.put(soneId, now); - Optional followedSone = getSone(soneId); - if (!followedSone.isPresent()) { - return; - } - for (Post post : followedSone.get().getPosts()) { - if (post.getTime() < now) { - markPostKnown(post); - } + if (newFriend) { + long now = System.currentTimeMillis(); + Optional followedSone = getSone(soneId); + if (!followedSone.isPresent()) { + return; + } + for (Post post : followedSone.get().getPosts()) { + if (post.getTime() < now) { + markPostKnown(post); } - for (PostReply reply : followedSone.get().getReplies()) { - if (reply.getTime() < now) { - markReplyKnown(reply); - } + } + for (PostReply reply : followedSone.get().getReplies()) { + if (reply.getTime() < now) { + markReplyKnown(reply); } } } - touchConfiguration(); } /** @@ -730,16 +722,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, checkNotNull(sone, "sone must not be null"); checkNotNull(soneId, "soneId must not be null"); database.removeFriend(sone, soneId); - boolean unfollowedSoneStillFollowed = false; - for (Sone localSone : getLocalSones()) { - unfollowedSoneStillFollowed |= localSone.hasFriend(soneId); - } - if (!unfollowedSoneStillFollowed) { - synchronized (soneFollowingTimes) { - soneFollowingTimes.remove(soneId); - } - } - touchConfiguration(); } /** @@ -1276,17 +1258,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, try { preferences.saveTo(configuration); - /* save Sone following times. */ - int soneCounter = 0; - synchronized (soneFollowingTimes) { - for (Entry soneFollowingTime : soneFollowingTimes.entrySet()) { - configuration.getStringValue("SoneFollowingTimes/" + soneCounter + "/Sone").setValue(soneFollowingTime.getKey()); - configuration.getLongValue("SoneFollowingTimes/" + soneCounter + "/Time").setValue(soneFollowingTime.getValue()); - ++soneCounter; - } - configuration.getStringValue("SoneFollowingTimes/" + soneCounter + "/Sone").setValue(null); - } - /* save known posts. */ database.save(); @@ -1309,20 +1280,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, */ private void loadConfiguration() { new PreferencesLoader(preferences).loadFrom(configuration); - - /* load Sone following times. */ - int soneCounter = 0; - while (true) { - String soneId = configuration.getStringValue("SoneFollowingTimes/" + soneCounter + "/Sone").getValue(null); - if (soneId == null) { - break; - } - long time = configuration.getLongValue("SoneFollowingTimes/" + soneCounter + "/Time").getValue(Long.MAX_VALUE); - synchronized (soneFollowingTimes) { - soneFollowingTimes.put(soneId, time); - } - ++soneCounter; - } } /** diff --git a/src/main/java/net/pterodactylus/sone/database/FriendProvider.java b/src/main/java/net/pterodactylus/sone/database/FriendProvider.java index 7583c77..cae2ff8 100644 --- a/src/main/java/net/pterodactylus/sone/database/FriendProvider.java +++ b/src/main/java/net/pterodactylus/sone/database/FriendProvider.java @@ -5,6 +5,8 @@ import java.util.Collection; import net.pterodactylus.sone.data.LocalSone; import net.pterodactylus.sone.data.Sone; +import com.google.common.base.Optional; + /** * Provides information about {@link Sone#getFriends() friends} of a {@link Sone}. * @@ -13,6 +15,7 @@ import net.pterodactylus.sone.data.Sone; public interface FriendProvider { Collection getFriends(LocalSone localSone); + Optional getSoneFollowingTime(String remoteSoneId); boolean isFriend(LocalSone localSone, String friendSoneId); } diff --git a/src/main/java/net/pterodactylus/sone/database/memory/ConfigurationLoader.java b/src/main/java/net/pterodactylus/sone/database/memory/ConfigurationLoader.java index fa8a362..0e37dab 100644 --- a/src/main/java/net/pterodactylus/sone/database/memory/ConfigurationLoader.java +++ b/src/main/java/net/pterodactylus/sone/database/memory/ConfigurationLoader.java @@ -3,7 +3,10 @@ package net.pterodactylus.sone.database.memory; import static java.util.logging.Level.WARNING; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import java.util.logging.Logger; @@ -43,6 +46,37 @@ public class ConfigurationLoader { saveIds("KnownSones", knownSones); } + public synchronized Map loadSoneFollowingTimes() { + Map soneFollowingTimes = new HashMap(); + int counter = 0; + while (true) { + String soneId = configuration.getStringValue("SoneFollowingTimes/" + counter + "/Sone").getValue(null); + if (soneId == null) { + break; + } + long followingTime = configuration.getLongValue("SoneFollowingTimes/" + counter + "/Time").getValue(Long.MAX_VALUE); + soneFollowingTimes.put(soneId, followingTime); + counter++; + } + return soneFollowingTimes; + } + + public synchronized void saveSoneFollowingTimes(Map soneFollowingTimes) { + try { + int counter = 0; + for (Entry soneFollowingTime : soneFollowingTimes.entrySet()) { + configuration.getStringValue("SoneFollowingTimes/" + counter + "/Sone") + .setValue(soneFollowingTime.getKey()); + configuration.getLongValue("SoneFollowingTimes/" + counter + "/Time") + .setValue(soneFollowingTime.getValue()); + counter++; + } + configuration.getStringValue("SoneFollowingTimes/" + counter + "/Sone").setValue(null); + } catch (ConfigurationException ce1) { + logger.log(WARNING, "Could not save Sone following times!", ce1); + } + } + public synchronized Set loadKnownPosts() { return loadIds("KnownPosts"); } diff --git a/src/main/java/net/pterodactylus/sone/database/memory/MemoryDatabase.java b/src/main/java/net/pterodactylus/sone/database/memory/MemoryDatabase.java index 983f8da..2d8d1be 100644 --- a/src/main/java/net/pterodactylus/sone/database/memory/MemoryDatabase.java +++ b/src/main/java/net/pterodactylus/sone/database/memory/MemoryDatabase.java @@ -464,6 +464,7 @@ public class MemoryDatabase extends AbstractService implements Database { @Override protected void doStart() { soneDatabase.start(); + memoryFriendDatabase.start(); postDatabase.start(); memoryBookmarkDatabase.start(); loadKnownPostReplies(); @@ -475,6 +476,7 @@ public class MemoryDatabase extends AbstractService implements Database { protected void doStop() { try { soneDatabase.stop(); + memoryFriendDatabase.stop(); postDatabase.stop(); memoryBookmarkDatabase.stop(); save(); @@ -632,6 +634,11 @@ public class MemoryDatabase extends AbstractService implements Database { } @Override + public Optional getSoneFollowingTime(String remoteSoneId) { + return memoryFriendDatabase.getSoneFollowingTime(remoteSoneId); + } + + @Override public boolean isFriend(LocalSone localSone, String friendSoneId) { if (!localSone.isLocal()) { return false; diff --git a/src/main/java/net/pterodactylus/sone/database/memory/MemoryFriendDatabase.java b/src/main/java/net/pterodactylus/sone/database/memory/MemoryFriendDatabase.java index 0be8738..8df477a 100644 --- a/src/main/java/net/pterodactylus/sone/database/memory/MemoryFriendDatabase.java +++ b/src/main/java/net/pterodactylus/sone/database/memory/MemoryFriendDatabase.java @@ -1,9 +1,12 @@ package net.pterodactylus.sone.database.memory; import java.util.Collection; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; +import com.google.common.base.Optional; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; @@ -17,11 +20,40 @@ class MemoryFriendDatabase { private final ReadWriteLock lock = new ReentrantReadWriteLock(); private final ConfigurationLoader configurationLoader; private final Multimap soneFriends = HashMultimap.create(); + private final Map soneFollowingTimes = new HashMap(); MemoryFriendDatabase(ConfigurationLoader configurationLoader) { this.configurationLoader = configurationLoader; } + void start() { + loadSoneFollowingTimes(); + } + + private void loadSoneFollowingTimes() { + Map soneFollowingTimes = configurationLoader.loadSoneFollowingTimes(); + lock.writeLock().lock(); + try { + this.soneFollowingTimes.clear(); + this.soneFollowingTimes.putAll(soneFollowingTimes); + } finally { + lock.writeLock().unlock(); + } + } + + void stop() { + saveSoneFollowingTimes(); + } + + private void saveSoneFollowingTimes() { + lock.readLock().lock(); + try { + configurationLoader.saveSoneFollowingTimes(soneFollowingTimes); + } finally { + lock.readLock().unlock(); + } + } + Collection getFriends(String localSoneId) { loadFriends(localSoneId); lock.readLock().lock(); @@ -32,6 +64,15 @@ class MemoryFriendDatabase { } } + Optional getSoneFollowingTime(String soneId) { + lock.readLock().lock(); + try { + return Optional.fromNullable(soneFollowingTimes.get(soneId)); + } finally { + lock.readLock().unlock(); + } + } + boolean isFriend(String localSoneId, String friendSoneId) { loadFriends(localSoneId); lock.readLock().lock(); @@ -46,6 +87,10 @@ class MemoryFriendDatabase { loadFriends(localSoneId); lock.writeLock().lock(); try { + if (!soneFollowingTimes.containsKey(friendSoneId)) { + soneFollowingTimes.put(friendSoneId, System.currentTimeMillis()); + saveSoneFollowingTimes(); + } if (soneFriends.put(localSoneId, friendSoneId)) { configurationLoader.saveFriends(localSoneId, soneFriends.get(localSoneId)); } @@ -59,6 +104,10 @@ class MemoryFriendDatabase { lock.writeLock().lock(); try { if (soneFriends.remove(localSoneId, friendSoneId)) { + if (!soneFriends.containsValue(friendSoneId)) { + soneFollowingTimes.remove(friendSoneId); + saveSoneFollowingTimes(); + } configurationLoader.saveFriends(localSoneId, soneFriends.get(localSoneId)); } } finally { diff --git a/src/test/java/net/pterodactylus/sone/database/memory/MemoryDatabaseTest.java b/src/test/java/net/pterodactylus/sone/database/memory/MemoryDatabaseTest.java index 1d7d471..8da6498 100644 --- a/src/test/java/net/pterodactylus/sone/database/memory/MemoryDatabaseTest.java +++ b/src/test/java/net/pterodactylus/sone/database/memory/MemoryDatabaseTest.java @@ -29,7 +29,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.emptyIterable; import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; @@ -54,6 +53,7 @@ import net.pterodactylus.sone.TestPostReplyBuilder; import net.pterodactylus.sone.TestValue; import net.pterodactylus.sone.data.Album; import net.pterodactylus.sone.data.Image; +import net.pterodactylus.sone.data.LocalSone; import net.pterodactylus.sone.data.Post; import net.pterodactylus.sone.data.PostReply; import net.pterodactylus.sone.data.Sone; @@ -81,8 +81,9 @@ public class MemoryDatabaseTest { private final Configuration configuration = mock(Configuration.class); private final MemoryDatabase memoryDatabase = new MemoryDatabase(null, configuration); private final Sone sone = mock(Sone.class); - private final Map> configurationValues = - new HashMap>(); + private final LocalSone localSone = mock(LocalSone.class); + private final Map> stringValues = new HashMap>(); + private final Map> longValues = new HashMap>(); @Before public void prepareConfigurationValues() { @@ -90,24 +91,43 @@ public class MemoryDatabaseTest { @Override public Value answer(InvocationOnMock invocation) throws Throwable { final String key = (String) invocation.getArguments()[0]; - if (!configurationValues.containsKey(key)) { + if (!stringValues.containsKey(key)) { TestValue value = Mockito.spy(new TestValue(null) { @Override public void setValue(String newValue) throws ConfigurationException { super.setValue(newValue); - configurationValues.put(key, this); + stringValues.put(key, this); } }); - configurationValues.put(key, value); + stringValues.put(key, value); } - return configurationValues.get(key); + return stringValues.get(key); + } + }); + when(configuration.getLongValue(anyString())).thenAnswer(new Answer>() { + @Override + public Value answer(InvocationOnMock invocation) throws Throwable { + final String key = (String) invocation.getArguments()[0]; + if (!longValues.containsKey(key)) { + TestValue value = Mockito.spy(new TestValue(null) { + @Override + public void setValue(Long newValue) throws ConfigurationException { + super.setValue(newValue); + longValues.put(key, this); + } + }); + longValues.put(key, value); + } + return longValues.get(key); } }); } @Before - public void setupSone() { + public void setupSones() { when(sone.getId()).thenReturn(SONE_ID); + when(localSone.getId()).thenReturn(SONE_ID); + when(localSone.isLocal()).thenReturn(true); } @Test @@ -305,78 +325,57 @@ public class MemoryDatabaseTest { } private void initializeFriends() { - when(configuration.getStringValue("Sone/" + SONE_ID + "/Friends/0/ID")).thenReturn( - TestValue.from("Friend1")); - when(configuration.getStringValue("Sone/" + SONE_ID + "/Friends/1/ID")).thenReturn( - TestValue.from("Friend2")); - when(configuration.getStringValue("Sone/" + SONE_ID + "/Friends/2/ID")).thenReturn( - TestValue.from(null)); + stringValues.put("Sone/" + SONE_ID + "/Friends/0/ID", + Mockito.spy(TestValue.from("Friend1"))); + stringValues.put("Sone/" + SONE_ID + "/Friends/1/ID", + Mockito.spy(TestValue.from("Friend2"))); + stringValues.put("Sone/" + SONE_ID + "/Friends/2/ID", + Mockito.spy(TestValue.from(null))); } @Test public void friendsAreReturnedCorrectly() { initializeFriends(); - when(sone.isLocal()).thenReturn(true); - Collection friends = memoryDatabase.getFriends(sone); + Collection friends = memoryDatabase.getFriends(localSone); assertThat(friends, containsInAnyOrder("Friend1", "Friend2")); } @Test public void friendsAreOnlyLoadedOnceFromConfiguration() { friendsAreReturnedCorrectly(); - memoryDatabase.getFriends(sone); + memoryDatabase.getFriends(localSone); verify(configuration).getStringValue("Sone/" + SONE_ID + "/Friends/0/ID"); } @Test - public void friendsAreOnlyReturnedForLocalSones() { - Collection friends = memoryDatabase.getFriends(sone); - assertThat(friends, emptyIterable()); - verify(configuration, never()).getStringValue("Sone/" + SONE_ID + "/Friends/0/ID"); - } - - @Test public void checkingForAFriendReturnsTrue() { initializeFriends(); when(sone.isLocal()).thenReturn(true); - assertThat(memoryDatabase.isFriend(sone, "Friend1"), is(true)); + assertThat(memoryDatabase.isFriend(localSone, "Friend1"), is(true)); } @Test public void checkingForAFriendThatIsNotAFriendReturnsFalse() { initializeFriends(); when(sone.isLocal()).thenReturn(true); - assertThat(memoryDatabase.isFriend(sone, "FriendX"), is(false)); - } - - @Test - public void checkingForAFriendOfRemoteSoneReturnsFalse() { - initializeFriends(); - assertThat(memoryDatabase.isFriend(sone, "Friend1"), is(false)); + assertThat(memoryDatabase.isFriend(localSone, "FriendX"), is(false)); } @Test public void friendIsAddedCorrectlyToLocalSone() throws ConfigurationException { when(sone.isLocal()).thenReturn(true); - memoryDatabase.addFriend(sone, "Friend1"); - assertThat(configurationValues.get("Sone/" + SONE_ID + "/Friends/0/ID").getValue(), + memoryDatabase.addFriend(localSone, "Friend1"); + assertThat(stringValues.get("Sone/" + SONE_ID + "/Friends/0/ID").getValue(), is("Friend1")); - assertThat(configurationValues.get("Sone/" + SONE_ID + "/Friends/1/ID").getValue(), + assertThat(stringValues.get("Sone/" + SONE_ID + "/Friends/1/ID").getValue(), nullValue()); } @Test - public void friendIsNotAddedToRemoteSone() throws ConfigurationException { - memoryDatabase.addFriend(sone, "Friend1"); - verify(configuration.getStringValue("Sone/" + SONE_ID + "/Friends/0/ID"), never()).setValue( - anyString()); - } - - @Test public void configurationIsWrittenOnceIfFriendIsAddedTwice() throws ConfigurationException { when(sone.isLocal()).thenReturn(true); - memoryDatabase.addFriend(sone, "Friend1"); - memoryDatabase.addFriend(sone, "Friend1"); + memoryDatabase.addFriend(localSone, "Friend1"); + memoryDatabase.addFriend(localSone, "Friend1"); verify(configuration.getStringValue("Sone/" + SONE_ID + "/Friends/0/ID")).setValue( anyString()); } @@ -384,19 +383,19 @@ public class MemoryDatabaseTest { @Test public void friendIsRemovedCorrectlyFromLocalSone() throws ConfigurationException { when(sone.isLocal()).thenReturn(true); - memoryDatabase.addFriend(sone, "Friend1"); - memoryDatabase.removeFriend(sone, "Friend1"); - assertThat(configurationValues.get("Sone/" + SONE_ID + "/Friends/0/ID").getValue(), + memoryDatabase.addFriend(localSone, "Friend1"); + memoryDatabase.removeFriend(localSone, "Friend1"); + assertThat(stringValues.get("Sone/" + SONE_ID + "/Friends/0/ID").getValue(), nullValue()); - assertThat(configurationValues.get("Sone/" + SONE_ID + "/Friends/1/ID").getValue(), + assertThat(stringValues.get("Sone/" + SONE_ID + "/Friends/1/ID").getValue(), nullValue()); } @Test public void configurationIsNotWrittenWhenANonFriendIsRemoved() throws ConfigurationException { when(sone.isLocal()).thenReturn(true); - memoryDatabase.removeFriend(sone, "Friend1"); - verify(configurationValues.get("Sone/" + SONE_ID + "/Friends/0/ID"), never()).setValue( + memoryDatabase.removeFriend(localSone, "Friend1"); + verify(stringValues.get("Sone/" + SONE_ID + "/Friends/0/ID"), never()).setValue( anyString()); } @@ -404,13 +403,13 @@ public class MemoryDatabaseTest { public void newDatabaseKnowsNoSones() { memoryDatabase.startAndWait(); assertThat(memoryDatabase.isSoneKnown(sone), is(false)); - assertThat(configurationValues, hasKey("KnownSones/0/ID")); - assertThat(configurationValues, not(hasKey("KnownSones/1/ID"))); + assertThat(stringValues, hasKey("KnownSones/0/ID")); + assertThat(stringValues, not(hasKey("KnownSones/1/ID"))); } @Test public void databaseLoadsKnownSonesCorrectly() { - configurationValues.put("KnownSones/0/ID", TestValue.from(SONE_ID)); + stringValues.put("KnownSones/0/ID", TestValue.from(SONE_ID)); memoryDatabase.startAndWait(); assertThat(memoryDatabase.isSoneKnown(sone), is(true)); } @@ -418,20 +417,62 @@ public class MemoryDatabaseTest { @Test public void databaseStoresKnownSonesCorrectly() throws ConfigurationException { memoryDatabase.setSoneKnown(sone); - assertThat(configurationValues, hasKey("KnownSones/0/ID")); - assertThat(configurationValues.get("KnownSones/0/ID").getValue(), is(SONE_ID)); - assertThat(configurationValues, hasKey("KnownSones/1/ID")); - assertThat(configurationValues.get("KnownSones/1/ID").getValue(), nullValue()); - assertThat(configurationValues, not(hasKey("KnownSones/2/ID"))); + assertThat(stringValues, hasKey("KnownSones/0/ID")); + assertThat(stringValues.get("KnownSones/0/ID").getValue(), is(SONE_ID)); + assertThat(stringValues, hasKey("KnownSones/1/ID")); + assertThat(stringValues.get("KnownSones/1/ID").getValue(), nullValue()); + assertThat(stringValues, not(hasKey("KnownSones/2/ID"))); } @Test public void stoppingTheDatabaseSavesTheKnownSones() throws ConfigurationException { - configurationValues.put("KnownSones/0/ID", Mockito.spy(TestValue.from(SONE_ID))); + stringValues.put("KnownSones/0/ID", Mockito.spy(TestValue.from(SONE_ID))); memoryDatabase.startAndWait(); memoryDatabase.stopAndWait(); - verify(configurationValues.get("KnownSones/0/ID")).setValue(SONE_ID); - verify(configurationValues.get("KnownSones/1/ID")).setValue(null); + verify(stringValues.get("KnownSones/0/ID")).setValue(SONE_ID); + verify(stringValues.get("KnownSones/1/ID")).setValue(null); + } + + @Test + public void soneFollowingTimesAreLoaded() { + stringValues.put("SoneFollowingTimes/0/Sone", TestValue.from(SONE_ID)); + longValues.put("SoneFollowingTimes/0/Time", TestValue.from(1000L)); + memoryDatabase.startAndWait(); + assertThat(memoryDatabase.getSoneFollowingTime(SONE_ID).get(), is(1000L)); + } + + @Test + public void soneFollowingTimeIsSetOnFirstFollowing() { + memoryDatabase.startAndWait(); + memoryDatabase.addFriend(localSone, "Friend1"); + assertThat(stringValues, hasKey("SoneFollowingTimes/0/Sone")); + assertThat(stringValues, hasKey("SoneFollowingTimes/1/Sone")); + assertThat(stringValues, not(hasKey("SoneFollowingTimes/2/Sone"))); + assertThat(longValues, hasKey("SoneFollowingTimes/0/Time")); + assertThat(longValues, not(hasKey("SoneFollowingTimes/1/Time"))); + } + + @Test + public void soneFollowingTimeIsNotSetOnSecondFollowing() throws ConfigurationException { + memoryDatabase.startAndWait(); + memoryDatabase.addFriend(localSone, "Friend1"); + LocalSone secondLocalSone = mock(LocalSone.class); + when(secondLocalSone.getId()).thenReturn("LocalSone2"); + long followingTime = longValues.get("SoneFollowingTimes/0/Time").getValue(); + memoryDatabase.addFriend(secondLocalSone, "Friend1"); + while (followingTime == System.currentTimeMillis()); + assertThat(longValues.get("SoneFollowingTimes/0/Time").getValue(), is(followingTime)); + } + + @Test + public void soneFollowingTimesAreRemovedWhenSoneIsUnfollowedByAll() + throws ConfigurationException { + stringValues.put("Sone/" + SONE_ID + "/Friends/0/ID", TestValue.from("Friend1")); + stringValues.put("SoneFollowingTimes/0/Sone", TestValue.from("Friend1")); + longValues.put("SoneFollowingTimes/0/Time", TestValue.from(1000L)); + memoryDatabase.startAndWait(); + memoryDatabase.removeFriend(localSone, "Friend1"); + assertThat(stringValues.get("SoneFollowingTimes/0/Sone").getValue(), nullValue()); } } -- 2.7.4